logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
commit: f5bf5ebb82e3af420dcd23d602b1be6cc86838e1
parent: 26bc5915727e0a0173c03cb49f5193dd612fb888
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Wed,  3 May 2017 02:04:16 +0200

Replace sprockets/browserify with Webpack (#2617)

* Replace browserify with webpack

* Add react-intl-translations-manager

* Do not minify in development, add offline-plugin for ServiceWorker background cache updates

* Adjust tests and dependencies

* Fix production deployments

* Fix tests

* More optimizations

* Improve travis cache for npm stuff

* Re-run travis

* Add back support for custom.scss as before

* Remove offline-plugin and babili

* Fix issue with Immutable.List().unshift(...values) not working as expected

* Make travis load schema instead of running all migrations in sequence

* Fix missing React import in WarningContainer. Optimize rendering performance by using ImmutablePureComponent instead of
React.PureComponent. ImmutablePureComponent uses Immutable.is() to compare props. Replace dynamic callback bindings in
<UI />

* Add react definitions to places that use JSX

* Add Procfile.dev for running rails, webpack and streaming API at the same time

Diffstat:

M.babelrc22++++++++++++++++++++--
A.foreman1+
M.gitignore4+++-
A.postcssrc.yml4++++
M.travis.yml7+++----
MDockerfile15+++++++--------
MGemfile28++++++++++++----------------
MGemfile.lock43+++++++++++--------------------------------
AProcfile.dev3+++
Dapp/assets/javascripts/application.js15---------------
Dapp/assets/javascripts/application_public.js9---------
Dapp/assets/javascripts/components.js15---------------
Dapp/assets/javascripts/components/components/account.jsx91-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/attachment_list.jsx32--------------------------------
Dapp/assets/javascripts/components/components/autosuggest_textarea.jsx211-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/avatar.jsx63---------------------------------------------------------------
Dapp/assets/javascripts/components/components/button.jsx49-------------------------------------------------
Dapp/assets/javascripts/components/components/collapsable.jsx20--------------------
Dapp/assets/javascripts/components/components/column_back_button.jsx31-------------------------------
Dapp/assets/javascripts/components/components/column_back_button_slim.jsx31-------------------------------
Dapp/assets/javascripts/components/components/column_collapsable.jsx56--------------------------------------------------------
Dapp/assets/javascripts/components/components/display_name.jsx24------------------------
Dapp/assets/javascripts/components/components/dropdown_menu.jsx78------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/extended_video_player.jsx53-----------------------------------------------------
Dapp/assets/javascripts/components/components/icon_button.jsx95-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/load_more.jsx14--------------
Dapp/assets/javascripts/components/components/loading_indicator.jsx9---------
Dapp/assets/javascripts/components/components/media_gallery.jsx195-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/missing_indicator.jsx9---------
Dapp/assets/javascripts/components/components/permalink.jsx36------------------------------------
Dapp/assets/javascripts/components/components/relative_timestamp.jsx19-------------------
Dapp/assets/javascripts/components/components/status.jsx121-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/status_action_bar.jsx137-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/status_content.jsx157-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/status_list.jsx128-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/components/video_player.jsx198-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/containers/mastodon.jsx320-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/containers/status_container.jsx117-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/account/components/action_bar.jsx92-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/account/components/header.jsx148-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/account_timeline/components/header.jsx81-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/account_timeline/containers/header_container.jsx75---------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/account_timeline/index.jsx87-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/blocks/index.jsx72------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/community_timeline/index.jsx95-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/autosuggest_account.jsx16----------------
Dapp/assets/javascripts/components/features/compose/components/autosuggest_status.jsx15---------------
Dapp/assets/javascripts/components/features/compose/components/character_counter.jsx26--------------------------
Dapp/assets/javascripts/components/features/compose/components/compose_form.jsx209-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx114-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/navigation_bar.jsx32--------------------------------
Dapp/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx104-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/reply_indicator.jsx69---------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/search.jsx82-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/search_results.jsx65-----------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/text_icon_button.jsx35-----------------------------------
Dapp/assets/javascripts/components/features/compose/components/upload_button.jsx60------------------------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/upload_form.jsx45---------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/upload_progress.jsx42------------------------------------------
Dapp/assets/javascripts/components/features/compose/components/warning.jsx25-------------------------
Dapp/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx50--------------------------------------------------
Dapp/assets/javascripts/components/features/compose/containers/warning_container.jsx48------------------------------------------------
Dapp/assets/javascripts/components/features/compose/index.jsx85-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/favourited_statuses/index.jsx66------------------------------------------------------------------
Dapp/assets/javascripts/components/features/favourites/index.jsx59-----------------------------------------------------------
Dapp/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx44--------------------------------------------
Dapp/assets/javascripts/components/features/follow_requests/index.jsx72------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/followers/index.jsx90-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/following/index.jsx90-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/generic_not_found/index.jsx10----------
Dapp/assets/javascripts/components/features/getting_started/index.jsx66------------------------------------------------------------------
Dapp/assets/javascripts/components/features/hashtag_timeline/index.jsx89-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/home_timeline/components/column_settings.jsx50--------------------------------------------------
Dapp/assets/javascripts/components/features/home_timeline/components/setting_text.jsx37-------------------------------------
Dapp/assets/javascripts/components/features/home_timeline/index.jsx37-------------------------------------
Dapp/assets/javascripts/components/features/mutes/index.jsx73-------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/notifications/components/clear_column_button.jsx26--------------------------
Dapp/assets/javascripts/components/features/notifications/components/column_settings.jsx70----------------------------------------------------------------------
Dapp/assets/javascripts/components/features/notifications/components/notification.jsx88-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/notifications/components/setting_toggle.jsx20--------------------
Dapp/assets/javascripts/components/features/notifications/index.jsx142-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/public_timeline/index.jsx95-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/reblogs/index.jsx59-----------------------------------------------------------
Dapp/assets/javascripts/components/features/report/components/status_check_box.jsx39---------------------------------------
Dapp/assets/javascripts/components/features/report/index.jsx130-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/status/components/action_bar.jsx101-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/status/components/card.jsx95-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/status/components/detailed_status.jsx94-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/status/index.jsx197-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/boost_modal.jsx82-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/column.jsx82-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/column_header.jsx42------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/column_link.jsx31-------------------------------
Dapp/assets/javascripts/components/features/ui/components/column_subheading.jsx15---------------
Dapp/assets/javascripts/components/features/ui/components/columns_area.jsx19-------------------
Dapp/assets/javascripts/components/features/ui/components/confirmation_modal.jsx50--------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/media_modal.jsx101-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/modal_root.jsx92-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/onboarding_modal.jsx263-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/tabs_bar.jsx23-----------------------
Dapp/assets/javascripts/components/features/ui/components/upload_area.jsx59-----------------------------------------------------------
Dapp/assets/javascripts/components/features/ui/components/video_modal.jsx38--------------------------------------
Dapp/assets/javascripts/components/features/ui/index.jsx166-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/ar.jsx131-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/bg.jsx68--------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/de.jsx126-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/en.jsx177-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/eo.jsx68--------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/es.jsx93-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/fa.jsx136-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/fi.jsx68--------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/fr.jsx155-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/he.jsx177-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/hr.jsx121-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/hu.jsx57---------------------------------------------------------
Dapp/assets/javascripts/components/locales/id.jsx167-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/index.jsx57---------------------------------------------------------
Dapp/assets/javascripts/components/locales/io.jsx126-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/it.jsx126-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/ja.jsx167-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/nl.jsx130-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/no.jsx130-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/oc.jsx128-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/pt-br.jsx125-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/pt.jsx125-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/ru.jsx138-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/uk.jsx57---------------------------------------------------------
Dapp/assets/javascripts/components/locales/zh-cn.jsx157-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/locales/zh-hk.jsx150-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/middleware/errors.jsx33---------------------------------
Dapp/assets/javascripts/components/middleware/loading_bar.jsx25-------------------------
Dapp/assets/javascripts/components/middleware/sounds.jsx22----------------------
Dapp/assets/javascripts/components/reducers/accounts.jsx131-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/alerts.jsx25-------------------------
Dapp/assets/javascripts/components/reducers/cards.jsx14--------------
Dapp/assets/javascripts/components/reducers/compose.jsx232-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/index.jsx36------------------------------------
Dapp/assets/javascripts/components/reducers/meta.jsx17-----------------
Dapp/assets/javascripts/components/reducers/modal.jsx18------------------
Dapp/assets/javascripts/components/reducers/notifications.jsx104-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/relationships.jsx38--------------------------------------
Dapp/assets/javascripts/components/reducers/reports.jsx60------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/search.jsx96-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/settings.jsx48------------------------------------------------
Dapp/assets/javascripts/components/reducers/status_lists.jsx39---------------------------------------
Dapp/assets/javascripts/components/reducers/statuses.jsx124-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/timelines.jsx317-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/reducers/user_lists.jsx80-------------------------------------------------------------------------------
Dapp/assets/javascripts/components/rtl.jsx27---------------------------
Dapp/assets/javascripts/components/selectors/index.jsx72------------------------------------------------------------------------
Dapp/assets/javascripts/components/store/configureStore.jsx16----------------
Dapp/assets/javascripts/components/stream.jsx22----------------------
Dapp/assets/javascripts/components/uuid.jsx3---
Dapp/assets/javascripts/extras.jsx49-------------------------------------------------
Rapp/assets/images/.keep -> app/assets/stylesheets/.gitkeep0
Dapp/assets/stylesheets/about.scss374-------------------------------------------------------------------------------
Dapp/assets/stylesheets/accounts.scss391-------------------------------------------------------------------------------
Dapp/assets/stylesheets/admin.scss245-------------------------------------------------------------------------------
Dapp/assets/stylesheets/application.scss21---------------------
Dapp/assets/stylesheets/basics.scss58----------------------------------------------------------
Dapp/assets/stylesheets/boost.scss11-----------
Dapp/assets/stylesheets/compact_header.scss28----------------------------
Dapp/assets/stylesheets/components.scss3180-------------------------------------------------------------------------------
Dapp/assets/stylesheets/containers.scss71-----------------------------------------------------------------------
Dapp/assets/stylesheets/fonts/montserrat.scss11-----------
Dapp/assets/stylesheets/fonts/roboto-mono.scss12------------
Dapp/assets/stylesheets/fonts/roboto.scss52----------------------------------------------------
Dapp/assets/stylesheets/footer.scss29-----------------------------
Dapp/assets/stylesheets/forms.scss335-------------------------------------------------------------------------------
Dapp/assets/stylesheets/landing_strip.scss17-----------------
Dapp/assets/stylesheets/lists.scss20--------------------
Dapp/assets/stylesheets/reset.scss91-------------------------------------------------------------------------------
Dapp/assets/stylesheets/rtl.scss136-------------------------------------------------------------------------------
Dapp/assets/stylesheets/stream_entries.scss372-------------------------------------------------------------------------------
Dapp/assets/stylesheets/tables.scss65-----------------------------------------------------------------
Dapp/assets/stylesheets/variables.scss8--------
Mapp/helpers/application_helper.rb6+++++-
Rapp/assets/fonts/montserrat/Montserrat-Regular.eot -> app/javascript/fonts/montserrat/Montserrat-Regular.eot0
Rapp/assets/fonts/montserrat/Montserrat-Regular.ttf -> app/javascript/fonts/montserrat/Montserrat-Regular.ttf0
Rapp/assets/fonts/montserrat/Montserrat-Regular.woff -> app/javascript/fonts/montserrat/Montserrat-Regular.woff0
Rapp/assets/fonts/montserrat/Montserrat-Regular.woff2 -> app/javascript/fonts/montserrat/Montserrat-Regular.woff20
Rapp/assets/fonts/roboto-mono/robotomono-regular-webfont.eot -> app/javascript/fonts/roboto-mono/robotomono-regular-webfont.eot0
Rapp/assets/fonts/roboto-mono/robotomono-regular-webfont.svg -> app/javascript/fonts/roboto-mono/robotomono-regular-webfont.svg0
Rapp/assets/fonts/roboto-mono/robotomono-regular-webfont.ttf -> app/javascript/fonts/roboto-mono/robotomono-regular-webfont.ttf0
Rapp/assets/fonts/roboto-mono/robotomono-regular-webfont.woff -> app/javascript/fonts/roboto-mono/robotomono-regular-webfont.woff0
Rapp/assets/fonts/roboto-mono/robotomono-regular-webfont.woff2 -> app/javascript/fonts/roboto-mono/robotomono-regular-webfont.woff20
Rapp/assets/fonts/roboto/roboto-bold-webfont.eot -> app/javascript/fonts/roboto/roboto-bold-webfont.eot0
Rapp/assets/fonts/roboto/roboto-bold-webfont.svg -> app/javascript/fonts/roboto/roboto-bold-webfont.svg0
Rapp/assets/fonts/roboto/roboto-bold-webfont.ttf -> app/javascript/fonts/roboto/roboto-bold-webfont.ttf0
Rapp/assets/fonts/roboto/roboto-bold-webfont.woff -> app/javascript/fonts/roboto/roboto-bold-webfont.woff0
Rapp/assets/fonts/roboto/roboto-bold-webfont.woff2 -> app/javascript/fonts/roboto/roboto-bold-webfont.woff20
Rapp/assets/fonts/roboto/roboto-italic-webfont.eot -> app/javascript/fonts/roboto/roboto-italic-webfont.eot0
Rapp/assets/fonts/roboto/roboto-italic-webfont.svg -> app/javascript/fonts/roboto/roboto-italic-webfont.svg0
Rapp/assets/fonts/roboto/roboto-italic-webfont.ttf -> app/javascript/fonts/roboto/roboto-italic-webfont.ttf0
Rapp/assets/fonts/roboto/roboto-italic-webfont.woff -> app/javascript/fonts/roboto/roboto-italic-webfont.woff0
Rapp/assets/fonts/roboto/roboto-italic-webfont.woff2 -> app/javascript/fonts/roboto/roboto-italic-webfont.woff20
Rapp/assets/fonts/roboto/roboto-medium-webfont.eot -> app/javascript/fonts/roboto/roboto-medium-webfont.eot0
Rapp/assets/fonts/roboto/roboto-medium-webfont.svg -> app/javascript/fonts/roboto/roboto-medium-webfont.svg0
Rapp/assets/fonts/roboto/roboto-medium-webfont.ttf -> app/javascript/fonts/roboto/roboto-medium-webfont.ttf0
Rapp/assets/fonts/roboto/roboto-medium-webfont.woff -> app/javascript/fonts/roboto/roboto-medium-webfont.woff0
Rapp/assets/fonts/roboto/roboto-medium-webfont.woff2 -> app/javascript/fonts/roboto/roboto-medium-webfont.woff20
Rapp/assets/fonts/roboto/roboto-regular-webfont.eot -> app/javascript/fonts/roboto/roboto-regular-webfont.eot0
Rapp/assets/fonts/roboto/roboto-regular-webfont.svg -> app/javascript/fonts/roboto/roboto-regular-webfont.svg0
Rapp/assets/fonts/roboto/roboto-regular-webfont.ttf -> app/javascript/fonts/roboto/roboto-regular-webfont.ttf0
Rapp/assets/fonts/roboto/roboto-regular-webfont.woff -> app/javascript/fonts/roboto/roboto-regular-webfont.woff0
Rapp/assets/fonts/roboto/roboto-regular-webfont.woff2 -> app/javascript/fonts/roboto/roboto-regular-webfont.woff20
Rapp/assets/javascripts/components/.gitkeep -> app/javascript/images/.keep0
Rapp/assets/images/background-photo.jpg -> app/javascript/images/background-photo.jpg0
Rapp/assets/images/boost_sprite.png -> app/javascript/images/boost_sprite.png0
Rapp/assets/images/elephant-friend.png -> app/javascript/images/elephant-friend.png0
Rapp/assets/images/fluffy-elephant-friend.png -> app/javascript/images/fluffy-elephant-friend.png0
Rapp/assets/images/logo.png -> app/javascript/images/logo.png0
Rapp/assets/images/logo.svg -> app/javascript/images/logo.svg0
Rapp/assets/images/mastodon-getting-started.png -> app/javascript/images/mastodon-getting-started.png0
Rapp/assets/images/mastodon-not-found.png -> app/javascript/images/mastodon-not-found.png0
Rapp/assets/images/mastodon.jpg -> app/javascript/images/mastodon.jpg0
Rapp/assets/images/mastodon_small.jpg -> app/javascript/images/mastodon_small.jpg0
Rapp/assets/images/screenshot.png -> app/javascript/images/screenshot.png0
Rapp/assets/images/void.png -> app/javascript/images/void.png0
Rapp/assets/images/.keep -> app/javascript/mastodon/.gitkeep0
Rapp/assets/javascripts/components/actions/accounts.jsx -> app/javascript/mastodon/actions/accounts.js0
Rapp/assets/javascripts/components/actions/alerts.jsx -> app/javascript/mastodon/actions/alerts.js0
Rapp/assets/javascripts/components/actions/blocks.jsx -> app/javascript/mastodon/actions/blocks.js0
Rapp/assets/javascripts/components/actions/cards.jsx -> app/javascript/mastodon/actions/cards.js0
Rapp/assets/javascripts/components/actions/compose.jsx -> app/javascript/mastodon/actions/compose.js0
Rapp/assets/javascripts/components/actions/favourites.jsx -> app/javascript/mastodon/actions/favourites.js0
Rapp/assets/javascripts/components/actions/interactions.jsx -> app/javascript/mastodon/actions/interactions.js0
Rapp/assets/javascripts/components/actions/modal.jsx -> app/javascript/mastodon/actions/modal.js0
Rapp/assets/javascripts/components/actions/mutes.jsx -> app/javascript/mastodon/actions/mutes.js0
Rapp/assets/javascripts/components/actions/notifications.jsx -> app/javascript/mastodon/actions/notifications.js0
Rapp/assets/javascripts/components/actions/onboarding.jsx -> app/javascript/mastodon/actions/onboarding.js0
Rapp/assets/javascripts/components/actions/reports.jsx -> app/javascript/mastodon/actions/reports.js0
Rapp/assets/javascripts/components/actions/search.jsx -> app/javascript/mastodon/actions/search.js0
Rapp/assets/javascripts/components/actions/settings.jsx -> app/javascript/mastodon/actions/settings.js0
Rapp/assets/javascripts/components/actions/statuses.jsx -> app/javascript/mastodon/actions/statuses.js0
Rapp/assets/javascripts/components/actions/store.jsx -> app/javascript/mastodon/actions/store.js0
Rapp/assets/javascripts/components/actions/timelines.jsx -> app/javascript/mastodon/actions/timelines.js0
Rapp/assets/javascripts/components/api.jsx -> app/javascript/mastodon/api.js0
Aapp/javascript/mastodon/components/account.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/attachment_list.js33+++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/autosuggest_textarea.js213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/avatar.js68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/button.js50++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/collapsable.js21+++++++++++++++++++++
Aapp/javascript/mastodon/components/column_back_button.js32++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/column_back_button_slim.js32++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/column_collapsable.js57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/display_name.js25+++++++++++++++++++++++++
Aapp/javascript/mastodon/components/dropdown_menu.js79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/extended_video_player.js54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/icon_button.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/load_more.js15+++++++++++++++
Aapp/javascript/mastodon/components/loading_indicator.js10++++++++++
Aapp/javascript/mastodon/components/media_gallery.js196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/missing_indicator.js12++++++++++++
Aapp/javascript/mastodon/components/permalink.js41+++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/relative_timestamp.js20++++++++++++++++++++
Aapp/javascript/mastodon/components/status.js123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/status_action_bar.js138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/status_content.js165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/status_list.js130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/components/video_player.js210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/containers/account_container.jsx -> app/javascript/mastodon/containers/account_container.js0
Aapp/javascript/mastodon/containers/mastodon.js314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/containers/status_container.js118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/emoji.jsx -> app/javascript/mastodon/emoji.js0
Aapp/javascript/mastodon/features/account/components/action_bar.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/account/components/header.js150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/account_timeline/components/header.js83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/account_timeline/containers/header_container.js76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/account_timeline/index.js89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/blocks/index.js74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/community_timeline/index.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/autosuggest_account.js26++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/character_counter.js27+++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/compose_form.js211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/navigation_bar.js37+++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/privacy_dropdown.js105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/reply_indicator.js71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/search.js82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/search_results.js67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/text_icon_button.js36++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/upload_button.js61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/upload_form.js46++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/upload_progress.js43+++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/components/warning.js26++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/compose/containers/autosuggest_account_container.jsx -> app/javascript/mastodon/features/compose/containers/autosuggest_account_container.js0
Rapp/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx -> app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js0
Rapp/assets/javascripts/components/features/compose/containers/compose_form_container.jsx -> app/javascript/mastodon/features/compose/containers/compose_form_container.js0
Rapp/assets/javascripts/components/features/compose/containers/navigation_container.jsx -> app/javascript/mastodon/features/compose/containers/navigation_container.js0
Rapp/assets/javascripts/components/features/compose/containers/privacy_dropdown_container.jsx -> app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js0
Rapp/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx -> app/javascript/mastodon/features/compose/containers/reply_indicator_container.js0
Rapp/assets/javascripts/components/features/compose/containers/search_container.jsx -> app/javascript/mastodon/features/compose/containers/search_container.js0
Rapp/assets/javascripts/components/features/compose/containers/search_results_container.jsx -> app/javascript/mastodon/features/compose/containers/search_results_container.js0
Aapp/javascript/mastodon/features/compose/containers/sensitive_button_container.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx -> app/javascript/mastodon/features/compose/containers/spoiler_button_container.js0
Rapp/assets/javascripts/components/features/compose/containers/upload_button_container.jsx -> app/javascript/mastodon/features/compose/containers/upload_button_container.js0
Rapp/assets/javascripts/components/features/compose/containers/upload_form_container.jsx -> app/javascript/mastodon/features/compose/containers/upload_form_container.js0
Rapp/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx -> app/javascript/mastodon/features/compose/containers/upload_progress_container.js0
Aapp/javascript/mastodon/features/compose/containers/warning_container.js49+++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/compose/index.js86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/favourited_statuses/index.js67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/favourites/index.js61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/follow_requests/components/account_authorize.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx -> app/javascript/mastodon/features/follow_requests/containers/account_authorize_container.js0
Aapp/javascript/mastodon/features/follow_requests/index.js74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/followers/index.js92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/following/index.js92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/generic_not_found/index.js11+++++++++++
Aapp/javascript/mastodon/features/getting_started/index.js73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/hashtag_timeline/index.js90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/home_timeline/components/column_settings.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/home_timeline/components/setting_text.js38++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/home_timeline/containers/column_settings_container.jsx -> app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js0
Aapp/javascript/mastodon/features/home_timeline/index.js38++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/mutes/index.js75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/notifications/components/clear_column_button.js27+++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/notifications/components/column_settings.js71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/notifications/components/notification.js90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/notifications/components/setting_toggle.js21+++++++++++++++++++++
Rapp/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx -> app/javascript/mastodon/features/notifications/containers/column_settings_container.js0
Rapp/assets/javascripts/components/features/notifications/containers/notification_container.jsx -> app/javascript/mastodon/features/notifications/containers/notification_container.js0
Aapp/javascript/mastodon/features/notifications/index.js143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/public_timeline/index.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/reblogs/index.js61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/report/components/status_check_box.js40++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/report/containers/status_check_box_container.jsx -> app/javascript/mastodon/features/report/containers/status_check_box_container.js0
Aapp/javascript/mastodon/features/report/index.js131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/status/components/action_bar.js102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/status/components/card.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/status/components/detailed_status.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/status/containers/card_container.jsx -> app/javascript/mastodon/features/status/containers/card_container.js0
Aapp/javascript/mastodon/features/status/index.js199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/boost_modal.js84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/column.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/column_header.js43+++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/column_link.js32++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/column_subheading.js16++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/columns_area.js20++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/confirmation_modal.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/media_modal.js103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/modal_root.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/onboarding_modal.js264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/tabs_bar.js24++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/upload_area.js60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/features/ui/components/video_modal.js40++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/features/ui/containers/loading_bar_container.jsx -> app/javascript/mastodon/features/ui/containers/loading_bar_container.js0
Rapp/assets/javascripts/components/features/ui/containers/modal_container.jsx -> app/javascript/mastodon/features/ui/containers/modal_container.js0
Rapp/assets/javascripts/components/features/ui/containers/notifications_container.jsx -> app/javascript/mastodon/features/ui/containers/notifications_container.js0
Rapp/assets/javascripts/components/features/ui/containers/status_list_container.jsx -> app/javascript/mastodon/features/ui/containers/status_list_container.js0
Aapp/javascript/mastodon/features/ui/index.js169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/is_mobile.jsx -> app/javascript/mastodon/is_mobile.js0
Rapp/assets/javascripts/components/link_header.jsx -> app/javascript/mastodon/link_header.js0
Aapp/javascript/mastodon/locales/ar.json172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/bg.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/de.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/defaultMessages.json1069+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/en.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/eo.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/es.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/fa.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/fi.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/fr.json163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/he.json165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/hr.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/hu.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/id.json167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/index.js57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/io.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/it.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/ja.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/nl.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/no.json163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/oc.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/pt-BR.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/pt.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/ru.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/uk.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/whitelist_ar.json3+++
Aapp/javascript/mastodon/locales/whitelist_bg.json3+++
Aapp/javascript/mastodon/locales/whitelist_de.json3+++
Aapp/javascript/mastodon/locales/whitelist_en.json3+++
Aapp/javascript/mastodon/locales/whitelist_eo.json3+++
Aapp/javascript/mastodon/locales/whitelist_es.json3+++
Aapp/javascript/mastodon/locales/whitelist_fa.json3+++
Aapp/javascript/mastodon/locales/whitelist_fi.json3+++
Aapp/javascript/mastodon/locales/whitelist_fr.json3+++
Aapp/javascript/mastodon/locales/whitelist_hr.json3+++
Aapp/javascript/mastodon/locales/whitelist_hu.json3+++
Aapp/javascript/mastodon/locales/whitelist_id.json3+++
Aapp/javascript/mastodon/locales/whitelist_io.json3+++
Aapp/javascript/mastodon/locales/whitelist_it.json3+++
Aapp/javascript/mastodon/locales/whitelist_ja.json3+++
Aapp/javascript/mastodon/locales/whitelist_nl.json3+++
Aapp/javascript/mastodon/locales/whitelist_no.json3+++
Aapp/javascript/mastodon/locales/whitelist_oc.json3+++
Aapp/javascript/mastodon/locales/whitelist_pt-BR.json3+++
Aapp/javascript/mastodon/locales/whitelist_pt.json3+++
Aapp/javascript/mastodon/locales/whitelist_ru.json3+++
Aapp/javascript/mastodon/locales/whitelist_uk.json3+++
Aapp/javascript/mastodon/locales/whitelist_zh-CN.json3+++
Aapp/javascript/mastodon/locales/whitelist_zh-HK.json3+++
Aapp/javascript/mastodon/locales/zh-CN.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/locales/zh-HK.json164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/middleware/errors.js33+++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/middleware/loading_bar.js25+++++++++++++++++++++++++
Aapp/javascript/mastodon/middleware/sounds.js22++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/accounts.js133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/accounts_counters.js135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/alerts.js25+++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/cards.js14++++++++++++++
Aapp/javascript/mastodon/reducers/compose.js232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/index.js38++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/meta.js17+++++++++++++++++
Aapp/javascript/mastodon/reducers/modal.js18++++++++++++++++++
Aapp/javascript/mastodon/reducers/notifications.js104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/relationships.js38++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/reports.js60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/search.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/settings.js52++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/status_lists.js39+++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/statuses.js124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/timelines.js317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/reducers/user_lists.js80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/rtl.js27+++++++++++++++++++++++++++
Aapp/javascript/mastodon/selectors/index.js73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/store/configureStore.js16++++++++++++++++
Aapp/javascript/mastodon/stream.js22++++++++++++++++++++++
Aapp/javascript/mastodon/uuid.js3+++
Aapp/javascript/packs/application.js29+++++++++++++++++++++++++++++
Aapp/javascript/packs/public.js53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/about.scss374+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/accounts.scss391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/admin.scss245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/application.scss20++++++++++++++++++++
Aapp/javascript/styles/basics.scss58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/boost.scss11+++++++++++
Aapp/javascript/styles/compact_header.scss28++++++++++++++++++++++++++++
Aapp/javascript/styles/components.scss3189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/containers.scss71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/fonts/montserrat.scss11+++++++++++
Aapp/javascript/styles/fonts/roboto-mono.scss12++++++++++++
Aapp/javascript/styles/fonts/roboto.scss52++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/footer.scss29+++++++++++++++++++++++++++++
Aapp/javascript/styles/forms.scss335+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/landing_strip.scss17+++++++++++++++++
Aapp/javascript/styles/lists.scss20++++++++++++++++++++
Aapp/javascript/styles/reset.scss91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/rtl.scss136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/stream_entries.scss372+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/tables.scss65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/styles/variables.scss8++++++++
Mapp/views/about/show.html.haml8++++----
Mapp/views/home/index.html.haml4++--
Mapp/views/layouts/admin.html.haml4++--
Mapp/views/layouts/application.html.haml4+++-
Mapp/views/layouts/auth.html.haml4++--
Mapp/views/layouts/embedded.html.haml2+-
Mapp/views/layouts/public.html.haml2+-
Abin/webpack33+++++++++++++++++++++++++++++++++
Abin/webpack-dev-server33+++++++++++++++++++++++++++++++++
Abin/yarn11+++++++++++
Mconfig/application.rb4----
Mconfig/environments/development.rb2--
Mconfig/environments/production.rb2--
Mconfig/initializers/assets.rb2+-
Aconfig/webpack/configuration.js26++++++++++++++++++++++++++
Aconfig/webpack/development.js16++++++++++++++++
Aconfig/webpack/development.server.js18++++++++++++++++++
Aconfig/webpack/development.server.yml17+++++++++++++++++
Aconfig/webpack/loaders/assets.js12++++++++++++
Aconfig/webpack/loaders/babel.js5+++++
Aconfig/webpack/loaders/coffee.js4++++
Aconfig/webpack/loaders/erb.js9+++++++++
Aconfig/webpack/loaders/sass.js14++++++++++++++
Aconfig/webpack/paths.yml33+++++++++++++++++++++++++++++++++
Aconfig/webpack/production.js44++++++++++++++++++++++++++++++++++++++++++++
Aconfig/webpack/shared.js59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/webpack/test.js6++++++
Aconfig/webpack/translationRunner.js34++++++++++++++++++++++++++++++++++
Mdocker-compose.yml17+++++++++++------
Mpackage.json58+++++++++++++++++++++++++++++++++++++++++++---------------
Mspec/controllers/api/v1/accounts_controller_spec.rb7++-----
Mspec/features/log_in_spec.rb2+-
Mspec/javascript/components/avatar.test.jsx2+-
Mspec/javascript/components/button.test.jsx2+-
Mspec/javascript/components/display_name.test.jsx2+-
Mspec/javascript/components/dropdown_menu.test.jsx2+-
Mspec/javascript/components/features/ui/components/column.test.jsx4++--
Mspec/javascript/components/loading_indicator.test.jsx2+-
Myarn.lock1762+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
492 files changed, 21500 insertions(+), 18238 deletions(-)

diff --git a/.babelrc b/.babelrc @@ -1,7 +1,25 @@ { - "presets": ["es2015", "react"], + "presets": [ + "es2015", + "react", + [ + "env", + { + "loose": true, + "modules": false + } + ] + ], "plugins": [ + "transform-react-jsx-source", + "transform-react-jsx-self", "transform-decorators-legacy", - "transform-object-rest-spread" + "transform-object-rest-spread", + [ + "react-intl", + { + "messagesDir": "./build/messages" + } + ] ] } diff --git a/.foreman b/.foreman @@ -0,0 +1 @@ +procfile: Procfile.dev diff --git a/.gitignore b/.gitignore @@ -22,7 +22,7 @@ public/assets .env .env.production node_modules/ -neo4j/ +build/ # Ignore Vagrant files .vagrant/ @@ -43,3 +43,5 @@ redis # Ignore vim files *~ *.swp +/public/packs +/node_modules diff --git a/.postcssrc.yml b/.postcssrc.yml @@ -0,0 +1,4 @@ +plugins: + postcss-smart-import: {} + precss: {} + autoprefixer: {} diff --git a/.travis.yml b/.travis.yml @@ -1,9 +1,7 @@ language: ruby cache: bundler: true - yarn: true - directories: - - node_modules + yarn: false dist: trusty sudo: false @@ -42,7 +40,8 @@ install: - yarn install before_script: - - bundle exec rails db:create db:migrate + - bundle exec rails db:create db:schema:load + - bundle exec rails assets:precompile script: - bundle exec rspec diff --git a/Dockerfile b/Dockerfile @@ -10,8 +10,6 @@ EXPOSE 3000 4000 WORKDIR /mastodon -COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/ - RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ && BUILD_DEPS=" \ postgresql-dev \ @@ -23,6 +21,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit $BUILD_DEPS \ nodejs@edge \ nodejs-npm@edge \ + git \ libpq \ libxml2 \ libxslt \ @@ -31,14 +30,14 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit imagemagick@edge \ ca-certificates \ && npm install -g npm@3 && npm install -g yarn \ - && bundle install --deployment --without test development \ - && yarn --ignore-optional \ - && yarn cache clean \ - && npm -g cache clean \ && update-ca-certificates \ - && apk del $BUILD_DEPS \ && rm -rf /tmp/* /var/cache/apk/* +COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/ + +RUN bundle install --deployment --without test development \ + && yarn --ignore-optional --pure-lockfile + COPY . /mastodon -VOLUME /mastodon/public/system /mastodon/public/assets +VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs diff --git a/Gemfile b/Gemfile @@ -5,22 +5,19 @@ ruby '>= 2.3.0', '< 2.5.0' gem 'pkg-config' +gem 'puma' gem 'rails', '~> 5.0.2' -gem 'sass-rails', '~> 5.0' gem 'uglifier', '>= 1.3.0' -gem 'jquery-rails' -gem 'puma' gem 'hamlit-rails' gem 'pg' gem 'pghero' gem 'dotenv-rails' -gem 'font-awesome-rails' gem 'best_in_place', '~> 3.0.1' +gem 'aws-sdk', '>= 2.0' gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder' -gem 'aws-sdk', '>= 2.0' gem 'addressable' gem 'devise' @@ -58,18 +55,18 @@ gem 'sprockets-rails', require: 'sprockets/railtie' gem 'statsd-instrument' gem 'twitter-text' gem 'tzinfo-data' +gem 'webpacker', '~>1.2' gem 'whatlanguage' +# For some reason the view specs start failing without this gem 'react-rails' -gem 'browserify-rails' -gem 'autoprefixer-rails' group :development, :test do - gem 'rspec-rails' - gem 'pry-rails' - gem 'fuubar' gem 'fabrication' + gem 'fuubar' gem 'i18n-tasks', '~> 0.9.6' + gem 'pry-rails' + gem 'rspec-rails' end group :test do @@ -83,24 +80,23 @@ group :test do end group :development do - gem 'rubocop', '0.46.0', require: false + gem 'active_record_query_trace' + gem 'annotate' gem 'better_errors' gem 'binding_of_caller' + gem 'bullet' gem 'letter_opener' gem 'letter_opener_web' - gem 'bullet' - gem 'active_record_query_trace' - gem 'annotate' + gem 'rubocop', '0.46.0', require: false gem 'capistrano', '3.8.0' gem 'capistrano-rails' gem 'capistrano-rbenv' gem 'capistrano-yarn' - gem 'capistrano-faster-assets', '~> 1.0' end group :production do + gem 'lograge' gem 'rails_12factor' gem 'redis-rails' - gem 'lograge' end diff --git a/Gemfile.lock b/Gemfile.lock @@ -43,15 +43,13 @@ GEM public_suffix (~> 2.0, >= 2.0.2) airbrussh (1.2.0) sshkit (>= 1.6.1, != 1.7.0) - annotate (2.7.1) - activerecord (>= 3.2, < 6.0) - rake (>= 10.4, < 12.0) + annotate (2.6.5) + activerecord (>= 2.3.0) + rake (>= 0.8.7) arel (7.1.4) ast (2.3.0) attr_encrypted (3.0.3) encryptor (~> 3.0.0) - autoprefixer-rails (6.7.7.2) - execjs av (0.9.0) cocaine (~> 0.5.3) aws-sdk (2.9.12) @@ -76,10 +74,6 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - browserify-rails (4.1.0) - addressable (>= 2.4.0) - railties (>= 4.0.0, < 5.1) - sprockets (>= 3.6.0) builder (3.2.3) bullet (5.5.1) activesupport (>= 3.0.0) @@ -92,8 +86,6 @@ GEM capistrano-bundler (1.2.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-faster-assets (1.0.2) - capistrano (>= 3.1) capistrano-rails (1.2.3) capistrano (~> 3.1) capistrano-bundler (~> 1.1) @@ -161,8 +153,6 @@ GEM faker (1.7.3) i18n (~> 0.5) fast_blank (1.0.0) - font-awesome-rails (4.7.0.1) - railties (>= 3.2, < 5.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -210,10 +200,6 @@ GEM rainbow (~> 2.2) terminal-table (>= 1.5.1) jmespath (1.3.1) - jquery-rails (4.3.1) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) json (2.1.0) kaminari (1.0.1) activesupport (>= 4.1.0) @@ -257,6 +243,7 @@ GEM mimemagic (0.3.2) mini_portile2 (2.1.0) minitest (5.10.1) + multi_json (1.12.1) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) @@ -348,8 +335,8 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - rake (11.3.0) - react-rails (1.11.0) + rake (12.0.0) + react-rails (2.1.0) babel-transpiler (>= 0.7.0) connection_pool execjs @@ -410,13 +397,6 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) - sass (3.4.23) - sass-rails (5.0.6) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) sidekiq (4.2.10) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) @@ -473,6 +453,10 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + webpacker (1.2) + activesupport (>= 4.2) + multi_json (~> 1.2) + railties (>= 4.2) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -487,15 +471,12 @@ DEPENDENCIES active_record_query_trace addressable annotate - autoprefixer-rails aws-sdk (>= 2.0) best_in_place (~> 3.0.1) better_errors binding_of_caller - browserify-rails bullet capistrano (= 3.8.0) - capistrano-faster-assets (~> 1.0) capistrano-rails capistrano-rbenv capistrano-yarn @@ -507,7 +488,6 @@ DEPENDENCIES fabrication faker fast_blank - font-awesome-rails fuubar goldfinger hamlit-rails @@ -517,7 +497,6 @@ DEPENDENCIES http_accept_language httplog i18n-tasks (~> 0.9.6) - jquery-rails kaminari letter_opener letter_opener_web @@ -554,7 +533,6 @@ DEPENDENCIES rubocop (= 0.46.0) ruby-oembed sanitize - sass-rails (~> 5.0) sidekiq sidekiq-unique-jobs simple-navigation @@ -566,6 +544,7 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) webmock + webpacker (~> 1.2) whatlanguage RUBY VERSION diff --git a/Procfile.dev b/Procfile.dev @@ -0,0 +1,3 @@ +web: bundle exec rails s -p 3000 +stream: yarn run start +webpack: ./bin/webpack-dev-server diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery2 -//= require jquery_ujs -//= require components diff --git a/app/assets/javascripts/application_public.js b/app/assets/javascripts/application_public.js @@ -1,9 +0,0 @@ -//= require jquery2 -//= require jquery_ujs -//= require extras -//= require best_in_place -//= require local_time - -$(function () { - $(".best_in_place").best_in_place(); -}); diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js @@ -1,15 +0,0 @@ -//= require_self -//= require react_ujs - -window.React = require('react'); -window.ReactDOM = require('react-dom'); -window.Perf = require('react-addons-perf'); - -if (!window.Intl) { - require('intl'); - require('intl/locale-data/jsonp/en.js'); -} - -//= require_tree ./components - -window.Mastodon = require('./components/containers/mastodon'); diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx @@ -1,91 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; - -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 @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' } -}); - -class Account extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleFollow = this.handleFollow.bind(this); - this.handleBlock = this.handleBlock.bind(this); - this.handleMute = this.handleMute.bind(this); - } - - handleFollow () { - this.props.onFollow(this.props.account); - } - - handleBlock () { - this.props.onBlock(this.props.account); - } - - handleMute () { - this.props.onMute(this.props.account); - } - - render () { - const { account, me, intl } = this.props; - - if (!account) { - return <div />; - } - - let buttons; - - if (account.get('id') !== me && account.get('relationship', null) !== null) { - 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 = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> - } else if (blocking) { - buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; - } else if (muting) { - buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; - } else { - buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; - } - } - - return ( - <div className='account'> - <div className='account__wrapper'> - <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> - <div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__relationship'> - {buttons} - </div> - </div> - </div> - ); - } - -} - -Account.propTypes = { - account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -} - -export default injectIntl(Account); diff --git a/app/assets/javascripts/components/components/attachment_list.jsx b/app/assets/javascripts/components/components/attachment_list.jsx @@ -1,32 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; - -class AttachmentList extends React.PureComponent { - - render () { - const { media } = this.props; - - return ( - <div className='attachment-list'> - <div className='attachment-list__icon'> - <i className='fa fa-link' /> - </div> - - <ul className='attachment-list__list'> - {media.map(attachment => - <li key={attachment.get('id')}> - <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a> - </li> - )} - </ul> - </div> - ); - } -} - -AttachmentList.propTypes = { - media: ImmutablePropTypes.list.isRequired -}; - -export default AttachmentList; diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx @@ -1,211 +0,0 @@ -import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from '../rtl'; - -const textAtCursorMatchesToken = (str, caretPosition) => { - let word; - - let left = str.slice(0, caretPosition).search(/\S+$/); - let right = str.slice(caretPosition).search(/\s/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 2 || word[0] !== '@') { - return [null, null]; - } - - word = word.trim().toLowerCase().slice(1); - - if (word.length > 0) { - return [left + 1, word]; - } else { - return [null, null]; - } -}; - -class AutosuggestTextarea extends React.Component { - - constructor (props, context) { - super(props, context); - this.state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0 - }; - this.onChange = this.onChange.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onSuggestionClick = this.onSuggestionClick.bind(this); - this.setTextarea = this.setTextarea.bind(this); - this.onPaste = this.onPaste.bind(this); - } - - onChange (e) { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - // auto-resize textarea - e.target.style.height = `${e.target.scrollHeight}px`; - - this.props.onChange(e); - } - - onKeyDown (e) { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - switch(e.key) { - case 'Escape': - if (!suggestionsHidden) { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - } - - onBlur () { - // If we hide the suggestions immediately, then this will prevent the - // onClick for the suggestions themselves from firing. - // Setting a short window for that to take place before hiding the - // suggestions ensures that can't happen. - setTimeout(() => { - this.setState({ suggestionsHidden: true }); - }, 100); - } - - onSuggestionClick (suggestion, e) { - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.textarea.focus(); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { - this.setState({ suggestionsHidden: false }); - } - } - - setTextarea (c) { - this.textarea = c; - } - - onPaste (e) { - if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.onPaste(e.clipboardData.files) - e.preventDefault(); - } - } - - reset () { - this.textarea.style.height = 'auto'; - } - - render () { - const { value, suggestions, disabled, placeholder, onKeyUp } = this.props; - const { suggestionsHidden, selectedSuggestion } = this.state; - const style = { direction: 'ltr' }; - - if (isRtl(value)) { - style.direction = 'rtl'; - } - - return ( - <div className='autosuggest-textarea'> - <textarea - ref={this.setTextarea} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={true} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={onKeyUp} - onBlur={this.onBlur} - onPaste={this.onPaste} - style={style} - /> - - <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> - {suggestions.map((suggestion, i) => ( - <div - role='button' - tabIndex='0' - key={suggestion} - className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} - onClick={this.onSuggestionClick.bind(this, suggestion)}> - <AutosuggestAccountContainer id={suggestion} /> - </div> - ))} - </div> - </div> - ); - } - -}; - -AutosuggestTextarea.propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - onPaste: PropTypes.func.isRequired, -}; - -export default AutosuggestTextarea; diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx @@ -1,63 +0,0 @@ -import PropTypes from 'prop-types'; - -class Avatar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - hovering: false - }; - this.handleMouseEnter = this.handleMouseEnter.bind(this); - this.handleMouseLeave = this.handleMouseLeave.bind(this); - } - - handleMouseEnter () { - this.setState({ hovering: true }); - } - - handleMouseLeave () { - this.setState({ hovering: false }); - } - - render () { - const { src, size, staticSrc, animate } = this.props; - const { hovering } = this.state; - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px` - }; - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - - return ( - <div - className='account__avatar' - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - style={style} - /> - ); - } - -} - -Avatar.propTypes = { - src: PropTypes.string.isRequired, - staticSrc: PropTypes.string, - size: PropTypes.number.isRequired, - style: PropTypes.object, - animate: PropTypes.bool -}; - -Avatar.defaultProps = { - animate: false -}; - -export default Avatar; diff --git a/app/assets/javascripts/components/components/button.jsx b/app/assets/javascripts/components/components/button.jsx @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; - -class Button extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - if (!this.props.disabled) { - this.props.onClick(); - } - } - - render () { - const style = { - display: this.props.block ? 'block' : 'inline-block', - width: this.props.block ? '100%' : 'auto', - padding: `0 ${this.props.size / 2.25}px`, - height: `${this.props.size}px`, - lineHeight: `${this.props.size}px` - }; - - return ( - <button className={`button ${this.props.secondary ? 'button-secondary' : ''}`} disabled={this.props.disabled} onClick={this.handleClick} style={{ ...style, ...this.props.style }}> - {this.props.text || this.props.children} - </button> - ); - } - -} - -Button.propTypes = { - text: PropTypes.node, - onClick: PropTypes.func, - disabled: PropTypes.bool, - block: PropTypes.bool, - secondary: PropTypes.bool, - size: PropTypes.number, - style: PropTypes.object, - children: PropTypes.node -}; - -Button.defaultProps = { - size: 36 -}; - -export default Button; diff --git a/app/assets/javascripts/components/components/collapsable.jsx b/app/assets/javascripts/components/components/collapsable.jsx @@ -1,20 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -const Collapsable = ({ fullHeight, isVisible, children }) => ( - <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}> - {({ opacity, height }) => - <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}> - {children} - </div> - } - </Motion> -); - -Collapsable.propTypes = { - fullHeight: PropTypes.number.isRequired, - isVisible: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired -}; - -export default Collapsable; diff --git a/app/assets/javascripts/components/components/column_back_button.jsx b/app/assets/javascripts/components/components/column_back_button.jsx @@ -1,31 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -class ColumnBackButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick () { - if (window.history && window.history.length === 1) this.context.router.push("/"); - else this.context.router.goBack(); - } - - render () { - return ( - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon'/> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> - ); - } - -}; - -ColumnBackButton.contextTypes = { - router: PropTypes.object -}; - -export default ColumnBackButton; diff --git a/app/assets/javascripts/components/components/column_back_button_slim.jsx b/app/assets/javascripts/components/components/column_back_button_slim.jsx @@ -1,31 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -class ColumnBackButtonSlim extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick () { - this.context.router.push('/'); - } - - render () { - return ( - <div className='column-back-button--slim'> - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> - </div> - ); - } -} - -ColumnBackButtonSlim.contextTypes = { - router: PropTypes.object -}; - -export default ColumnBackButtonSlim; diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -1,56 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -class ColumnCollapsable extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - collapsed: true - }; - - this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this); - } - - handleToggleCollapsed () { - const currentState = this.state.collapsed; - - this.setState({ collapsed: !currentState }); - - if (!currentState && this.props.onCollapse) { - this.props.onCollapse(); - } - } - - render () { - const { icon, title, fullHeight, children } = this.props; - const { collapsed } = this.state; - const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable'; - - return ( - <div className='column-collapsable'> - <div role='button' tabIndex='0' title={`${title}`} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}> - <i className={`fa fa-${icon}`} /> - </div> - - <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> - {({ opacity, height }) => - <div style={{ overflow: height === fullHeight ? 'auto' : 'hidden', height: `${height}px`, opacity: opacity / 100, maxHeight: '70vh' }}> - {children} - </div> - } - </Motion> - </div> - ); - } -} - -ColumnCollapsable.propTypes = { - icon: PropTypes.string.isRequired, - title: PropTypes.string, - fullHeight: PropTypes.number.isRequired, - children: PropTypes.node, - onCollapse: PropTypes.func -}; - -export default ColumnCollapsable; diff --git a/app/assets/javascripts/components/components/display_name.jsx b/app/assets/javascripts/components/components/display_name.jsx @@ -1,24 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; -import emojify from '../emoji'; - -class DisplayName extends React.PureComponent { - - render () { - const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - - return ( - <span className='display-name'> - <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> - </span> - ); - } - -}; - -DisplayName.propTypes = { - account: ImmutablePropTypes.map.isRequired -} - -export default DisplayName; diff --git a/app/assets/javascripts/components/components/dropdown_menu.jsx b/app/assets/javascripts/components/components/dropdown_menu.jsx @@ -1,78 +0,0 @@ -import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; -import PropTypes from 'prop-types'; - -class DropdownMenu extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - direction: 'left' - }; - this.setRef = this.setRef.bind(this); - this.renderItem = this.renderItem.bind(this); - } - - setRef (c) { - this.dropdown = c; - } - - handleClick (i, e) { - const { action } = this.props.items[i]; - - if (typeof action === 'function') { - e.preventDefault(); - action(); - this.dropdown.hide(); - } - } - - renderItem (item, i) { - if (item === null) { - return <li key={ 'sep' + i } className='dropdown__sep' />; - } - - const { text, action, href = '#' } = item; - - return ( - <li className='dropdown__content-list-item' key={ text + i }> - <a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'> - {text} - </a> - </li> - ); - } - - render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right"; - - return ( - <Dropdown ref={this.setRef}> - <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}> - <i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} /> - </DropdownTrigger> - - <DropdownContent className={directionClass}> - <ul className='dropdown__content-list'> - {items.map(this.renderItem)} - </ul> - </DropdownContent> - </Dropdown> - ); - } - -} - -DropdownMenu.propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, - direction: PropTypes.string, - ariaLabel: PropTypes.string -}; - -DropdownMenu.defaultProps = { - ariaLabel: "Menu" -}; - -export default DropdownMenu; diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; - -class ExtendedVideoPlayer extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleLoadedData = this.handleLoadedData.bind(this); - this.setRef = this.setRef.bind(this); - } - - handleLoadedData () { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount () { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount () { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef (c) { - this.video = c; - } - - render () { - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={this.props.src} - autoPlay - muted={this.props.muted} - controls={this.props.controls} - loop={!this.props.controls} - /> - </div> - ); - } - -} - -ExtendedVideoPlayer.propTypes = { - src: PropTypes.string.isRequired, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired -}; - -export default ExtendedVideoPlayer; diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx @@ -1,95 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -class IconButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - e.preventDefault(); - - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - render () { - let style = { - fontSize: `${this.props.size}px`, - width: `${this.props.size * 1.28571429}px`, - height: `${this.props.size * 1.28571429}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style - }; - - if (this.props.active) { - style = { ...style, ...this.props.activeStyle }; - } - - const classes = ['icon-button']; - - if (this.props.active) { - classes.push('active'); - } - - if (this.props.disabled) { - classes.push('disabled'); - } - - if (this.props.inverted) { - classes.push('inverted'); - } - - if (this.props.overlay) { - classes.push('overlayed'); - } - - if (this.props.className) { - classes.push(this.props.className) - } - - return ( - <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> - {({ rotate }) => - <button - aria-label={this.props.title} - title={this.props.title} - className={classes.join(' ')} - onClick={this.handleClick} - style={style}> - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> - </button> - } - </Motion> - ); - } - -} - -IconButton.propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool -}; - -IconButton.defaultProps = { - size: 18, - active: false, - disabled: false, - animate: false, - overlay: false -}; - -export default IconButton; diff --git a/app/assets/javascripts/components/components/load_more.jsx b/app/assets/javascripts/components/components/load_more.jsx @@ -1,14 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -const LoadMore = ({ onClick }) => ( - <a href="#" className='load-more' role='button' onClick={onClick}> - <FormattedMessage id='status.load_more' defaultMessage='Load more' /> - </a> -); - -LoadMore.propTypes = { - onClick: PropTypes.func -}; - -export default LoadMore; diff --git a/app/assets/javascripts/components/components/loading_indicator.jsx b/app/assets/javascripts/components/components/loading_indicator.jsx @@ -1,9 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -const LoadingIndicator = () => ( - <div className='loading-indicator'> - <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> - </div> -); - -export default LoadingIndicator; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx @@ -1,195 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from '../is_mobile'; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' } -}); - -class Item extends React.PureComponent { - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - const { index, onClick } = this.props; - - if (e.button === 0) { - e.preventDefault(); - onClick(index); - } - - e.stopPropagation(); - } - - render () { - const { attachment, index, size } = this.props; - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - let thumbnail = ''; - - if (attachment.get('type') === 'image') { - thumbnail = ( - <a - className='media-gallery__item-thumbnail' - href={attachment.get('remote_url') || attachment.get('url')} - onClick={this.handleClick} - target='_blank' - style={{ backgroundImage: `url(${attachment.get('preview_url')})` }} - /> - ); - } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && this.props.autoPlayGif; - - thumbnail = ( - <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}> - <video - className='media-gallery__item-gifv-thumbnail' - role='application' - src={attachment.get('url')} - onClick={this.handleClick} - autoPlay={autoPlay} - loop={true} - muted={true} - /> - - <span className='media-gallery__gifv__label'>GIF</span> - </div> - ); - } - - return ( - <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} - </div> - ); - } - -} - -Item.propTypes = { - attachment: ImmutablePropTypes.map.isRequired, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - onClick: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -class MediaGallery extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - visible: !props.sensitive - }; - this.handleOpen = this.handleOpen.bind(this); - this.handleClick = this.handleClick.bind(this); - } - - handleOpen (e) { - this.setState({ visible: !this.state.visible }); - } - - handleClick (index) { - this.props.onOpenMedia(this.props.media, index); - } - - render () { - const { media, intl, sensitive } = this.props; - - let children; - - if (!this.state.visible) { - let warning; - - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - - children = ( - <div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}> - <span className='media-spoiler__warning'>{warning}</span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } else { - const size = media.take(4).size; - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />); - } - - return ( - <div className='media-gallery' style={{ height: `${this.props.height}px` }}> - <div className='spoiler-button' style={{ display: !this.state.visible ? 'none' : 'block' }}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> - </div> - - {children} - </div> - ); - } - -} - -MediaGallery.propTypes = { - sensitive: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - height: PropTypes.number.isRequired, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -export default injectIntl(MediaGallery); diff --git a/app/assets/javascripts/components/components/missing_indicator.jsx b/app/assets/javascripts/components/components/missing_indicator.jsx @@ -1,9 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -const MissingIndicator = () => ( - <div className='missing-indicator'> - <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> - </div> -); - -export default MissingIndicator; diff --git a/app/assets/javascripts/components/components/permalink.jsx b/app/assets/javascripts/components/components/permalink.jsx @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; - -class Permalink extends React.Component { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(this.props.to); - } - } - - render () { - const { href, children, className, ...other } = this.props; - - return <a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>{children}</a>; - } - -} - -Permalink.contextTypes = { - router: PropTypes.object -}; - -Permalink.propTypes = { - className: PropTypes.string, - href: PropTypes.string.isRequired, - to: PropTypes.string.isRequired, - children: PropTypes.node -}; - -export default Permalink; diff --git a/app/assets/javascripts/components/components/relative_timestamp.jsx b/app/assets/javascripts/components/components/relative_timestamp.jsx @@ -1,19 +0,0 @@ -import { injectIntl, FormattedRelative } from 'react-intl'; -import PropTypes from 'prop-types'; - -const RelativeTimestamp = ({ intl, timestamp }) => { - const date = new Date(timestamp); - - return ( - <time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}> - <FormattedRelative value={date} /> - </time> - ); -}; - -RelativeTimestamp.propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired -}; - -export default injectIntl(RelativeTimestamp); diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx @@ -1,121 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import RelativeTimestamp from './relative_timestamp'; -import DisplayName from './display_name'; -import MediaGallery from './media_gallery'; -import VideoPlayer from './video_player'; -import AttachmentList from './attachment_list'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import { FormattedMessage } from 'react-intl'; -import emojify from '../emoji'; -import escapeTextContentForBrowser from 'escape-html'; - -class Status extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - this.handleAccountClick = this.handleAccountClick.bind(this); - } - - handleClick () { - const { status } = this.props; - this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); - } - - handleAccountClick (id, e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${id}`); - } - } - - render () { - let media = ''; - const { status, ...other } = this.props; - - if (status === null) { - return <div />; - } - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - let displayName = status.getIn(['account', 'display_name']); - - if (displayName.length === 0) { - displayName = status.getIn(['account', 'username']); - } - - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - - return ( - <div className='status__wrapper'> - <div className='status__prepend'> - <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> - <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> - </div> - - <Status {...other} wrapped={true} status={status.get('reblog')} /> - </div> - ); - } - - if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />; - } else { - media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />; - } - } - - return ( - <div className={this.props.muted ? 'status muted' : 'status'}> - <div className='status__info'> - <div className='status__info-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - - <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> - </div> - - <DisplayName account={status.get('account')} /> - </a> - </div> - - <StatusContent status={status} onClick={this.handleClick} /> - - {media} - - <StatusActionBar {...this.props} /> - </div> - ); - } - -} - -Status.contextTypes = { - router: PropTypes.object -}; - -Status.propTypes = { - status: ImmutablePropTypes.map, - wrapped: PropTypes.bool, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - me: PropTypes.number, - boostModal: PropTypes.bool, - autoPlayGif: PropTypes.bool, - muted: PropTypes.bool -}; - -export default Status; diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -1,137 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import DropdownMenu from './dropdown_menu'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - open: { id: 'status.open', defaultMessage: 'Expand this status' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' } -}); - -class StatusActionBar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleReplyClick = this.handleReplyClick.bind(this); - this.handleFavouriteClick = this.handleFavouriteClick.bind(this); - this.handleReblogClick = this.handleReblogClick.bind(this); - this.handleDeleteClick = this.handleDeleteClick.bind(this); - this.handleMentionClick = this.handleMentionClick.bind(this); - this.handleMuteClick = this.handleMuteClick.bind(this); - this.handleBlockClick = this.handleBlockClick.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleReport = this.handleReport.bind(this); - } - - handleReplyClick () { - this.props.onReply(this.props.status, this.context.router); - } - - handleFavouriteClick () { - this.props.onFavourite(this.props.status); - } - - handleReblogClick (e) { - this.props.onReblog(this.props.status, e); - } - - handleDeleteClick () { - this.props.onDelete(this.props.status); - } - - handleMentionClick () { - this.props.onMention(this.props.status.get('account'), this.context.router); - } - - handleMuteClick () { - this.props.onMute(this.props.status.get('account')); - } - - handleBlockClick () { - this.props.onBlock(this.props.status.get('account')); - } - - handleOpen () { - this.context.router.push(`/statuses/${this.props.status.get('id')}`); - } - - handleReport () { - this.props.onReport(this.props.status); - this.context.router.push('/report'); - } - - render () { - const { status, me, intl } = this.props; - const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; - let menu = []; - - menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - menu.push(null); - - if (status.getIn(['account', 'id']) === me) { - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - 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'; - let reply_icon; - let reply_title; - if (status.get('in_reply_to_id', null) === null) { - reply_icon = "reply"; - reply_title = intl.formatMessage(messages.reply); - } else { - reply_icon = "reply-all"; - reply_title = intl.formatMessage(messages.replyAll); - } - - return ( - <div className='status__action-bar'> - <div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div> - <div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> - <div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div> - - <div className='status__action-bar-dropdown'> - <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/> - </div> - </div> - ); - } - -} - -StatusActionBar.contextTypes = { - router: PropTypes.object -}; - -StatusActionBar.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onMention: PropTypes.func, - onMute: PropTypes.func, - onBlock: PropTypes.func, - onReport: PropTypes.func, - me: PropTypes.number.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(StatusActionBar); diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx @@ -1,157 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; -import PropTypes from 'prop-types'; -import emojify from '../emoji'; -import { isRtl } from '../rtl'; -import { FormattedMessage } from 'react-intl'; -import Permalink from './permalink'; - -class StatusContent extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - hidden: true - }; - this.onMentionClick = this.onMentionClick.bind(this); - this.onHashtagClick = this.onHashtagClick.bind(this); - this.handleMouseDown = this.handleMouseDown.bind(this) - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleSpoilerClick = this.handleSpoilerClick.bind(this); - }; - - componentDidMount () { - const node = ReactDOM.findDOMNode(this); - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); - let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || (item.get('remote_url').length > 0 && link.href === item.get('remote_url'))); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - } else if (media) { - link.innerHTML = '<i class="fa fa-fw fa-photo"></i>'; - } else { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - link.setAttribute('title', link.href); - } - } - } - - onMentionClick (mention, e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${mention.get('id')}`); - } - } - - onHashtagClick (hashtag, e) { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); - - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/timelines/tag/${hashtag}`); - } - } - - handleMouseDown (e) { - this.startXY = [e.clientX, e.clientY]; - } - - handleMouseUp (e) { - const [ startX, startY ] = this.startXY; - const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - - if (e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) { - return; - } - - if (deltaX + deltaY < 5 && e.button === 0) { - this.props.onClick(); - } - - this.startXY = null; - } - - handleSpoilerClick (e) { - e.preventDefault(); - this.setState({ hidden: !this.state.hidden }); - } - - render () { - const { status } = this.props; - const { hidden } = this.state; - - const content = { __html: emojify(status.get('content')) }; - const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; - const directionStyle = { direction: 'ltr' }; - - if (isRtl(status.get('content'))) { - directionStyle.direction = 'rtl'; - } - - if (status.get('spoiler_text').length > 0) { - let mentionsPlaceholder = ''; - - const mentionLinks = status.get('mentions').map(item => ( - <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'> - @<span>{item.get('username')}</span> - </Permalink> - )).reduce((aggregate, item) => [...aggregate, item, ' '], []) - - const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />; - - if (hidden) { - mentionsPlaceholder = <div>{mentionLinks}</div>; - } - - return ( - <div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> - <p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} > - <span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a> - </p> - - {mentionsPlaceholder} - - <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> - </div> - ); - } else if (this.props.onClick) { - return ( - <div - className='status__content' - style={{ ...directionStyle }} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - /> - ); - } else { - return ( - <div - className='status__content status__content--no-action' - style={{ ...directionStyle }} - dangerouslySetInnerHTML={content} - /> - ); - } - } - -} - -StatusContent.contextTypes = { - router: PropTypes.object -}; - -StatusContent.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onClick: PropTypes.func -}; - -export default StatusContent; diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx @@ -1,128 +0,0 @@ -import Status from './status'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { ScrollContainer } from 'react-router-scroll'; -import PropTypes from 'prop-types'; -import StatusContainer from '../containers/status_container'; -import LoadMore from './load_more'; - -class StatusList extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - this.setRef = this.setRef.bind(this); - this.handleLoadMore = this.handleLoadMore.bind(this); - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - const offset = scrollHeight - scrollTop - clientHeight; - this._oldScrollPosition = scrollHeight - scrollTop; - - if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) { - this.props.onScrollToBottom(); - } else if (scrollTop < 100 && this.props.onScrollToTop) { - this.props.onScrollToTop(); - } else if (this.props.onScroll) { - this.props.onScroll(); - } - } - - componentDidMount () { - this.attachScrollListener(); - } - - componentDidUpdate (prevProps) { - if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) { - this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition; - } - } - - componentWillUnmount () { - this.detachScrollListener(); - } - - attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - } - - detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - } - - setRef (c) { - this.node = c; - } - - handleLoadMore (e) { - e.preventDefault(); - this.props.onScrollToBottom(); - } - - render () { - const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props; - - let loadMore = ''; - let scrollableArea = ''; - let unread = ''; - - if (!isLoading && statusIds.size > 0 && hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - if (isUnread) { - unread = <div className='status-list__unread-indicator' />; - } - - if (isLoading || statusIds.size > 0 || !emptyMessage) { - scrollableArea = ( - <div className='scrollable' ref={this.setRef}> - {unread} - - <div className='status-list'> - {prepend} - - {statusIds.map((statusId) => { - return <StatusContainer key={statusId} id={statusId} />; - })} - - {loadMore} - </div> - </div> - ); - } else { - scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} - </div> - ); - } - - return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> - {scrollableArea} - </ScrollContainer> - ); - } - -} - -StatusList.propTypes = { - scrollKey: PropTypes.string.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node -}; - -StatusList.defaultProps = { - trackScroll: true -}; - -export default StatusList; diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx @@ -1,198 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from '../is_mobile'; - -const messages = defineMessages({ - toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, - toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }, - expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }, - expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' } -}); - -class VideoPlayer extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - visible: !this.props.sensitive, - preview: true, - muted: true, - hasAudio: true, - videoError: false - }; - - this.handleClick = this.handleClick.bind(this); - this.handleVideoClick = this.handleVideoClick.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleVisibility = this.handleVisibility.bind(this); - this.handleExpand = this.handleExpand.bind(this); - this.setRef = this.setRef.bind(this); - this.handleLoadedData = this.handleLoadedData.bind(this); - this.handleVideoError = this.handleVideoError.bind(this); - } - - handleClick () { - this.setState({ muted: !this.state.muted }); - } - - handleVideoClick (e) { - e.stopPropagation(); - - const node = ReactDOM.findDOMNode(this).querySelector('video'); - - if (node.paused) { - node.play(); - } else { - node.pause(); - } - } - - handleOpen () { - this.setState({ preview: !this.state.preview }); - } - - handleVisibility () { - this.setState({ - visible: !this.state.visible, - preview: true - }); - } - - handleExpand () { - this.video.pause(); - this.props.onOpenVideo(this.props.media, this.video.currentTime); - } - - setRef (c) { - this.video = c; - } - - handleLoadedData () { - if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) { - this.setState({ hasAudio: false }); - } - } - - handleVideoError () { - this.setState({ videoError: true }); - } - - componentDidMount () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentDidUpdate () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentWillUnmount () { - if (!this.video) { - return; - } - - this.video.removeEventListener('loadeddata', this.handleLoadedData); - this.video.removeEventListener('error', this.handleVideoError); - } - - render () { - const { media, intl, width, height, sensitive, autoplay } = this.props; - - let spoilerButton = ( - <div className='status__video-player-spoiler' style={{ display: !this.state.visible ? 'none' : 'block' }} > - <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> - </div> - ); - - let expandButton = ( - <div className='status__video-player-expand'> - <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} /> - </div> - ); - - let muteButton = ''; - - if (this.state.hasAudio) { - muteButton = ( - <div className='status__video-player-mute'> - <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /> - </div> - ); - } - - if (!this.state.visible) { - if (sensitive) { - return ( - <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> - {spoilerButton} - <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } else { - return ( - <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> - {spoilerButton} - <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } - } - - if (this.state.preview && !autoplay) { - return ( - <div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center` }} onClick={this.handleOpen}> - {spoilerButton} - <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> - </div> - ); - } - - if (this.state.videoError) { - return ( - <div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' > - <span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span> - </div> - ); - } - - return ( - <div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}> - {spoilerButton} - {muteButton} - {expandButton} - <video className='status__video-player-video' role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} onClick={this.handleVideoClick} /> - </div> - ); - } - -} - -VideoPlayer.propTypes = { - media: ImmutablePropTypes.map.isRequired, - width: PropTypes.number, - height: PropTypes.number, - sensitive: PropTypes.bool, - intl: PropTypes.object.isRequired, - autoplay: PropTypes.bool, - onOpenVideo: PropTypes.func.isRequired -}; - -VideoPlayer.defaultProps = { - width: 239, - height: 110 -}; - -export default injectIntl(VideoPlayer); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx @@ -1,320 +0,0 @@ -import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from '../store/configureStore'; -import { - refreshTimelineSuccess, - updateTimeline, - deleteFromTimelines, - refreshTimeline, - connectTimeline, - disconnectTimeline -} from '../actions/timelines'; -import { showOnboardingOnce } from '../actions/onboarding'; -import { updateNotifications, refreshNotifications } from '../actions/notifications'; -import createBrowserHistory from 'history/lib/createBrowserHistory'; -import { - applyRouterMiddleware, - useRouterHistory, - Router, - Route, - IndexRedirect, - IndexRoute -} from 'react-router'; -import { useScroll } from 'react-router-scroll'; -import UI from '../features/ui'; -import Status from '../features/status'; -import GettingStarted from '../features/getting_started'; -import PublicTimeline from '../features/public_timeline'; -import CommunityTimeline from '../features/community_timeline'; -import AccountTimeline from '../features/account_timeline'; -import HomeTimeline from '../features/home_timeline'; -import Compose from '../features/compose'; -import Followers from '../features/followers'; -import Following from '../features/following'; -import Reblogs from '../features/reblogs'; -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 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 ar from 'react-intl/locale-data/ar'; -import en from 'react-intl/locale-data/en'; -import de from 'react-intl/locale-data/de'; -import eo from 'react-intl/locale-data/eo'; -import es from 'react-intl/locale-data/es'; -import fa from 'react-intl/locale-data/fa'; -import fi from 'react-intl/locale-data/fi'; -import fr from 'react-intl/locale-data/fr'; -import he from 'react-intl/locale-data/he'; -import hu from 'react-intl/locale-data/hu'; -import it from 'react-intl/locale-data/it'; -import ja from 'react-intl/locale-data/ja'; -import pt from 'react-intl/locale-data/pt'; -import nl from 'react-intl/locale-data/nl'; -import no from 'react-intl/locale-data/no'; -import ru from 'react-intl/locale-data/ru'; -import uk from 'react-intl/locale-data/uk'; -import zh from 'react-intl/locale-data/zh'; -import bg from 'react-intl/locale-data/bg'; -import id from 'react-intl/locale-data/id'; -import { localeData as zh_hk } from '../locales/zh-hk'; -import { localeData as zh_cn } from '../locales/zh-cn'; -import pt_br from '../locales/pt-br'; -import getMessagesForLocale from '../locales'; -import { hydrateStore } from '../actions/store'; -import createStream from '../stream'; - -const store = configureStore(); -const initialState = JSON.parse(document.getElementById("initial-state").textContent); -store.dispatch(hydrateStore(initialState)); - -const browserHistory = useRouterHistory(createBrowserHistory)({ - basename: '/web' -}); - -addLocaleData([ - ...en, - ...ar, - ...de, - ...eo, - ...es, - ...fa, - ...fi, - ...fr, - ...he, - ...hu, - ...it, - ...ja, - ...pt, - ...pt_br, - ...nl, - ...no, - ...ru, - ...uk, - ...zh, - ...zh_hk, - ...zh_cn, - ...bg, - ...id, -]); - -const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0]; - -const hiddenColumnContainerStyle = { - position: 'absolute', - left: '0', - top: '0', - visibility: 'hidden' -}; - -class Container extends React.PureComponent { - - constructor(props) { - super(props); - - this.state = { - renderedPersistents: [], - unrenderedPersistents: [], - }; - } - - componentWillMount () { - this.unlistenHistory = null; - - this.setState(() => { - return { - mountImpersistent: false, - renderedPersistents: [], - unrenderedPersistents: [ - {pathname: '/timelines/home', component: HomeTimeline}, - {pathname: '/timelines/public', component: PublicTimeline}, - {pathname: '/timelines/public/local', component: CommunityTimeline}, - - {pathname: '/notifications', component: Notifications}, - {pathname: '/favourites', component: FavouritedStatuses} - ], - }; - }, () => { - if (this.unlistenHistory) { - return; - } - - this.unlistenHistory = browserHistory.listen(location => { - const pathname = location.pathname.replace(/\/$/, '').toLowerCase(); - - this.setState(oldState => { - let persistentMatched = false; - - const newState = { - renderedPersistents: oldState.renderedPersistents.map(persistent => { - const givenMatched = persistent.pathname === pathname; - - if (givenMatched) { - persistentMatched = true; - } - - return { - hidden: !givenMatched, - pathname: persistent.pathname, - component: persistent.component - }; - }), - }; - - if (!persistentMatched) { - newState.unrenderedPersistents = []; - - oldState.unrenderedPersistents.forEach(persistent => { - if (persistent.pathname === pathname) { - persistentMatched = true; - - newState.renderedPersistents.push({ - hidden: false, - pathname: persistent.pathname, - component: persistent.component - }); - } else { - newState.unrenderedPersistents.push(persistent); - } - }); - } - - newState.mountImpersistent = !persistentMatched; - - return newState; - }); - }); - }); - } - - componentWillUnmount () { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - - this.unlistenHistory = "done"; - } - - render () { - // Hide some components rather than unmounting them to allow to show again - // quickly and keep the view state such as the scrolled offset. - const persistentsView = this.state.renderedPersistents.map((persistent) => - <div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}> - <persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} /> - </div> - ); - - return ( - <UI> - {this.state.mountImpersistent && this.props.children} - {persistentsView} - </UI> - ); - } -} - -Container.propTypes = { - children: PropTypes.node, -}; - -class Mastodon extends React.Component { - - 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(streamingAPIBaseURL, accessToken, 'user', { - - connected () { - store.dispatch(connectTimeline('home')); - }, - - disconnected () { - store.dispatch(disconnectTimeline('home')); - }, - - received (data) { - switch(data.event) { - case 'update': - store.dispatch(updateTimeline('home', JSON.parse(data.payload))); - break; - case 'delete': - store.dispatch(deleteFromTimelines(data.payload)); - break; - case 'notification': - store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale)); - break; - } - }, - - reconnected () { - store.dispatch(connectTimeline('home')); - store.dispatch(refreshTimeline('home')); - store.dispatch(refreshNotifications()); - } - - }); - - // Desktop notifications - if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { - Notification.requestPermission(); - } - - store.dispatch(showOnboardingOnce()); - } - - componentWillUnmount () { - if (typeof this.subscription !== 'undefined') { - this.subscription.close(); - this.subscription = null; - } - } - - render () { - const { locale } = this.props; - - return ( - <IntlProvider locale={locale} messages={getMessagesForLocale(locale)}> - <Provider store={store}> - <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}> - <Route path='/' component={Container}> - <IndexRedirect to="/getting-started" /> - - <Route path='getting-started' component={GettingStarted} /> - <Route path='timelines/tag/:id' component={HashtagTimeline} /> - - <Route path='statuses/new' component={Compose} /> - <Route path='statuses/:statusId' component={Status} /> - <Route path='statuses/:statusId/reblogs' component={Reblogs} /> - <Route path='statuses/:statusId/favourites' component={Favourites} /> - - <Route path='accounts/:accountId' component={AccountTimeline} /> - <Route path='accounts/:accountId/followers' component={Followers} /> - <Route path='accounts/:accountId/following' component={Following} /> - - <Route path='follow_requests' component={FollowRequests} /> - <Route path='blocks' component={Blocks} /> - <Route path='mutes' component={Mutes} /> - <Route path='report' component={Report} /> - - <Route path='*' component={GenericNotFound} /> - </Route> - </Router> - </Provider> - </IntlProvider> - ); - } - -} - -Mastodon.propTypes = { - locale: PropTypes.string.isRequired -}; - -export default Mastodon; diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx @@ -1,117 +0,0 @@ -import { connect } from 'react-redux'; -import Status from '../components/status'; -import { makeGetStatus } from '../selectors'; -import { - replyCompose, - mentionCompose -} from '../actions/compose'; -import { - reblog, - favourite, - unreblog, - unfavourite -} from '../actions/interactions'; -import { - blockAccount, - muteAccount -} from '../actions/accounts'; -import { deleteStatus } from '../actions/statuses'; -import { initReport } from '../actions/reports'; -import { openModal } from '../actions/modal'; -import { createSelector } from 'reselect' -import { isMobile } from '../is_mobile' -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props.id), - me: state.getIn(['meta', 'me']), - boostModal: state.getIn(['meta', 'boost_modal']), - autoPlayGif: state.getIn(['meta', 'auto_play_gif']) - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onReply (status, router) { - dispatch(replyCompose(status, router)); - }, - - onModalReblog (status) { - dispatch(reblog(status)); - }, - - onReblog (status, e) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else { - if (e.shiftKey || !this.boostModal) { - this.onModalReblog(status); - } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); - } - } - }, - - onFavourite (status) { - if (status.get('favourited')) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }, - - onDelete (status) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))) - })); - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); - }, - - onOpenVideo (media, time) { - dispatch(openModal('VIDEO', { media, time })); - }, - - onBlock (account) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))) - })); - }, - - onReport (status) { - dispatch(initReport(status.get('account'), status)); - }, - - onMute (account) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))) - })); - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -1,92 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenu from '../../../components/dropdown_menu'; -import { Link } from 'react-router'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; - -const messages = defineMessages({ - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } -}); - -class ActionBar extends React.PureComponent { - - render () { - const { account, me, intl } = this.props; - - let menu = []; - let extraInfo = ''; - - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); - menu.push(null); - - if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - } else { - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); - } - - if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); - } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } - - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); - } - - if (account.get('acct') !== account.get('username')) { - extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>; - } - - return ( - <div className='account__action-bar'> - <div className='account__action-bar-dropdown'> - <DropdownMenu items={menu} icon='bars' size={24} direction="right" /> - </div> - - <div className='account__action-bar-links'> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> - <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span> - <strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> - <span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span> - <strong><FormattedNumber value={account.get('following_count')} /> {extraInfo}</strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> - <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> - <strong><FormattedNumber value={account.get('followers_count')} /> {extraInfo}</strong> - </Link> - </div> - </div> - ); - } - -} - -ActionBar.propTypes = { - account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(ActionBar); diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx @@ -1,148 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import emojify from '../../../emoji'; -import escapeTextContentForBrowser from 'escape-html'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import IconButton from '../../../components/icon_button'; -import { Motion, spring } from 'react-motion'; -import { connect } from 'react-redux'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } -}); - -const makeMapStateToProps = () => { - const mapStateToProps = (state, props) => ({ - autoPlayGif: state.getIn(['meta', 'auto_play_gif']) - }); - - return mapStateToProps; -}; - -class Avatar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - - this.state = { - isHovered: false - }; - - this.handleMouseOver = this.handleMouseOver.bind(this); - this.handleMouseOut = this.handleMouseOut.bind(this); - } - - handleMouseOver () { - if (this.state.isHovered) return; - this.setState({ isHovered: true }); - } - - handleMouseOut () { - if (!this.state.isHovered) return; - this.setState({ isHovered: false }); - } - - render () { - const { account, autoPlayGif } = this.props; - const { isHovered } = this.state; - - return ( - <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> - {({ radius }) => - <a - href={account.get('url')} - className='account__header__avatar' - target='_blank' - rel='noopener' - style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }} - onMouseOver={this.handleMouseOver} - onMouseOut={this.handleMouseOut} - onFocus={this.handleMouseOver} - onBlur={this.handleMouseOut} - /> - } - </Motion> - ); - } - -} - -Avatar.propTypes = { - account: ImmutablePropTypes.map.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -class Header extends React.Component { - - render () { - const { account, me, intl } = this.props; - - if (!account) { - return null; - } - - let displayName = account.get('display_name'); - let info = ''; - let actionBtn = ''; - let lockedIcon = ''; - - if (displayName.length === 0) { - displayName = account.get('username'); - } - - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span> - } - - if (me !== account.get('id')) { - if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( - <div style={{ position: 'absolute', top: '10px', left: '20px' }}> - <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> - </div> - ); - } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ( - <div style={{ position: 'absolute', top: '10px', left: '20px' }}> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); - } - } - - if (account.get('locked')) { - lockedIcon = <i className='fa fa-lock' />; - } - - const content = { __html: emojify(account.get('note')) }; - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - - return ( - <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> - <div style={{ padding: '20px 10px' }}> - <Avatar account={account} autoPlayGif={this.props.autoPlayGif} /> - - <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} /> - <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span> - <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} /> - - {info} - {actionBtn} - </div> - </div> - ); - } - -} - -Header.propTypes = { - account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -export default connect(makeMapStateToProps)(injectIntl(Header)); diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx @@ -1,81 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import InnerHeader from '../../account/components/header'; -import ActionBar from '../../account/components/action_bar'; -import MissingIndicator from '../../../components/missing_indicator'; - -class Header extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleFollow = this.handleFollow.bind(this); - this.handleBlock = this.handleBlock.bind(this); - this.handleMention = this.handleMention.bind(this); - this.handleReport = this.handleReport.bind(this); - this.handleMute = this.handleMute.bind(this); - } - - handleFollow () { - this.props.onFollow(this.props.account); - } - - handleBlock () { - this.props.onBlock(this.props.account); - } - - handleMention () { - this.props.onMention(this.props.account, this.context.router); - } - - handleReport () { - this.props.onReport(this.props.account); - this.context.router.push('/report'); - } - - handleMute() { - this.props.onMute(this.props.account); - } - - render () { - const { account, me } = this.props; - - if (account === null) { - return <MissingIndicator />; - } - - return ( - <div className='account-timeline__header'> - <InnerHeader - account={account} - me={me} - onFollow={this.handleFollow} - /> - - <ActionBar - account={account} - me={me} - onBlock={this.handleBlock} - onMention={this.handleMention} - onReport={this.handleReport} - onMute={this.handleMute} - /> - </div> - ); - } -} - -Header.propTypes = { - account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired -}; - -Header.contextTypes = { - router: PropTypes.object -}; - -export default Header; diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx @@ -1,75 +0,0 @@ -import { connect } from 'react-redux'; -import { makeGetAccount } from '../../../selectors'; -import Header from '../components/header'; -import { - followAccount, - unfollowAccount, - blockAccount, - unblockAccount, - muteAccount, - unmuteAccount -} from '../../../actions/accounts'; -import { mentionCompose } from '../../../actions/compose'; -import { initReport } from '../../../actions/reports'; -import { openModal } from '../../../actions/modal'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -const messages = defineMessages({ - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' } -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, Number(accountId)), - me: state.getIn(['meta', 'me']) - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onFollow (account) { - if (account.getIn(['relationship', 'following'])) { - dispatch(unfollowAccount(account.get('id'))); - } else { - dispatch(followAccount(account.get('id'))); - } - }, - - onBlock (account) { - if (account.getIn(['relationship', 'blocking'])) { - dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))) - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onReport (account) { - dispatch(initReport(account)); - }, - - onMute (account) { - if (account.getIn(['relationship', 'muting'])) { - dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))) - })); - } - } -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx @@ -1,87 +0,0 @@ -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { - fetchAccount, - fetchAccountTimeline, - expandAccountTimeline -} from '../../actions/accounts'; -import StatusList from '../../components/status_list'; -import LoadingIndicator from '../../components/loading_indicator'; -import Column from '../ui/components/column'; -import HeaderContainer from './containers/header_container'; -import ColumnBackButton from '../../components/column_back_button'; -import Immutable from 'immutable'; - -const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()), - isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']), - hasMore: !!state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'next']), - me: state.getIn(['meta', 'me']) -}); - -class AccountTimeline extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScrollToBottom = this.handleScrollToBottom.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId))); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchAccountTimeline(Number(nextProps.params.accountId))); - } - } - - handleScrollToBottom () { - if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId))); - } - } - - render () { - const { statusIds, isLoading, hasMore, me } = this.props; - - if (!statusIds && isLoading) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <StatusList - prepend={<HeaderContainer accountId={this.props.params.accountId} />} - scrollKey='account_timeline' - statusIds={statusIds} - isLoading={isLoading} - hasMore={hasMore} - me={me} - onScrollToBottom={this.handleScrollToBottom} - /> - </Column> - ); - } - -} - -AccountTimeline.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - me: PropTypes.number.isRequired -}; - -export default connect(mapStateToProps)(AccountTimeline); diff --git a/app/assets/javascripts/components/features/blocks/index.jsx b/app/assets/javascripts/components/features/blocks/index.jsx @@ -1,72 +0,0 @@ -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -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 { fetchBlocks, expandBlocks } from '../../actions/blocks'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - heading: { id: 'column.blocks', defaultMessage: 'Blocked users' } -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'blocks', 'items']) -}); - -class Blocks extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchBlocks()); - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandBlocks()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column icon='ban' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='blocks'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } -} - -Blocks.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired -}; - -export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -1,95 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; -import Column from '../ui/components/column'; -import { - refreshTimeline, - updateTimeline, - deleteFromTimelines, - connectTimeline, - disconnectTimeline -} from '../../actions/timelines'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; -import createStream from '../../stream'; - -const messages = defineMessages({ - title: { id: 'column.community', defaultMessage: 'Local timeline' } -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, - streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), - accessToken: state.getIn(['meta', 'access_token']) -}); - -let subscription; - -class CommunityTimeline extends React.PureComponent { - - componentDidMount () { - const { dispatch, streamingAPIBaseURL, accessToken } = this.props; - - dispatch(refreshTimeline('community')); - - if (typeof subscription !== 'undefined') { - return; - } - - subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', { - - connected () { - dispatch(connectTimeline('community')); - }, - - reconnected () { - dispatch(connectTimeline('community')); - }, - - disconnected () { - dispatch(disconnectTimeline('community')); - }, - - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('community', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } - } - - }); - } - - componentWillUnmount () { - // if (typeof subscription !== 'undefined') { - // subscription.close(); - // subscription = null; - // } - } - - render () { - const { intl, hasUnread } = this.props; - - return ( - <Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}> - <ColumnBackButtonSlim /> - <StatusListContainer {...this.props} scrollKey='community_timeline' type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} /> - </Column> - ); - } - -} - -CommunityTimeline.propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - streamingAPIBaseURL: PropTypes.string.isRequired, - accessToken: PropTypes.string.isRequired, - hasUnread: PropTypes.bool -}; - -export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx @@ -1,16 +0,0 @@ -import Avatar from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const AutosuggestAccount = ({ account }) => ( - <div className='autosuggest-account'> - <div className='autosuggest-account-icon'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div> - <DisplayName account={account} /> - </div> -); - -AutosuggestAccount.propTypes = { - account: ImmutablePropTypes.map.isRequired -}; - -export default AutosuggestAccount; diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx @@ -1,15 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import DisplayName from '../../../components/display_name'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const AutosuggestStatus = ({ status }) => ( - <div className='autosuggest-status'> - <FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} /> - </div> -); - -AutosuggestStatus.propTypes = { - status: ImmutablePropTypes.map.isRequired -}; - -export default AutosuggestStatus; diff --git a/app/assets/javascripts/components/features/compose/components/character_counter.jsx b/app/assets/javascripts/components/features/compose/components/character_counter.jsx @@ -1,26 +0,0 @@ -import PropTypes from 'prop-types'; -import { length } from 'stringz'; - -class CharacterCounter extends React.PureComponent { - - checkRemainingText (diff) { - if (diff < 0) { - return <span className='character-counter character-counter--over'>{diff}</span>; - } - return <span className='character-counter'>{diff}</span>; - } - - render () { - const diff = this.props.max - length(this.props.text); - - return this.checkRemainingText(diff); - } - -} - -CharacterCounter.propTypes = { - text: PropTypes.string.isRequired, - max: PropTypes.number.isRequired -} - -export default CharacterCounter; diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -1,209 +0,0 @@ -import CharacterCounter from './character_counter'; -import Button from '../../../components/button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import { debounce } from 'react-decoration'; -import UploadButtonContainer from '../containers/upload_button_container'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; -import Collapsable from '../../../components/collapsable'; -import SpoilerButtonContainer from '../containers/spoiler_button_container'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; -import EmojiPickerDropdown from './emoji_picker_dropdown'; -import UploadFormContainer from '../containers/upload_form_container'; -import TextIconButton from './text_icon_button'; -import WarningContainer from '../containers/warning_container'; - -const messages = defineMessages({ - placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' }, - publish: { id: 'compose_form.publish', defaultMessage: 'Toot' } -}); - -class ComposeForm extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleChange = this.handleChange.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this); - this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); - this.onSuggestionSelected = this.onSuggestionSelected.bind(this); - this.handleChangeSpoilerText = this.handleChangeSpoilerText.bind(this); - this.setAutosuggestTextarea = this.setAutosuggestTextarea.bind(this); - this.handleEmojiPick = this.handleEmojiPick.bind(this); - } - - handleChange (e) { - this.props.onChange(e.target.value); - } - - handleKeyDown (e) { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit () { - this.autosuggestTextarea.reset(); - this.props.onSubmit(); - } - - onSuggestionsClearRequested () { - this.props.onClearSuggestions(); - } - - @debounce(500) - onSuggestionsFetchRequested (token) { - this.props.onFetchSuggestions(token); - } - - onSuggestionSelected (tokenStart, token, value) { - this._restoreCaret = null; - this.props.onSuggestionSelected(tokenStart, token, value); - } - - handleChangeSpoilerText (e) { - this.props.onChangeSpoilerText(e.target.value); - } - - componentWillReceiveProps (nextProps) { - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - if (!nextProps.is_uploading && this.props.is_uploading) { - this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; - } - } - - componentDidUpdate (prevProps) { - // 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; - // this provides a convenient shortcut to drop everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved caret position, - // restores the cursor to that position after the text changes! - if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { - let selectionEnd, selectionStart; - - if (this.props.preselectDate !== prevProps.preselectDate) { - selectionEnd = this.props.text.length; - selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; - } else { - selectionEnd = this.props.text.length; - selectionStart = selectionEnd; - } - - this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); - this.autosuggestTextarea.textarea.focus(); - } - } - - setAutosuggestTextarea (c) { - this.autosuggestTextarea = c; - } - - handleEmojiPick (data) { - const position = this.autosuggestTextarea.textarea.selectionStart; - this._restoreCaret = position + data.shortname.length + 1; - this.props.onPickEmoji(position, data); - } - - render () { - const { intl, onPaste } = this.props; - const disabled = this.props.is_submitting; - const text = [this.props.spoiler_text, this.props.text].join(''); - - let publishText = ''; - let reply_to_other = false; - - if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; - } else { - publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== 'unlisted' ? '!' : ''); - } - - return ( - <div className='compose-form'> - <Collapsable isVisible={this.props.spoiler} fullHeight={50}> - <div className="spoiler-input"> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" id='cw-spoiler-input'/> - </div> - </Collapsable> - - <WarningContainer /> - - <ReplyIndicatorContainer /> - - <div className='compose-form__autosuggest-wrapper'> - <AutosuggestTextarea - ref={this.setAutosuggestTextarea} - placeholder={intl.formatMessage(messages.placeholder)} - disabled={disabled} - value={this.props.text} - onChange={this.handleChange} - suggestions={this.props.suggestions} - onKeyDown={this.handleKeyDown} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - onPaste={onPaste} - /> - - <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> - </div> - - <div className='compose-form__modifiers'> - <UploadFormContainer /> - </div> - - <div className='compose-form__buttons-wrapper'> - <div className='compose-form__buttons'> - <UploadButtonContainer /> - <PrivacyDropdownContainer /> - <SensitiveButtonContainer /> - <SpoilerButtonContainer /> - </div> - - <div className='compose-form__publish'> - <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> - <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500 || (text.length !==0 && text.trim().length === 0)} block /></div> - </div> - </div> - </div> - ); - } - -} - -ComposeForm.propTypes = { - intl: PropTypes.object.isRequired, - text: PropTypes.string.isRequired, - suggestion_token: PropTypes.string, - suggestions: ImmutablePropTypes.list, - spoiler: PropTypes.bool, - privacy: PropTypes.string, - spoiler_text: PropTypes.string, - focusDate: PropTypes.instanceOf(Date), - preselectDate: PropTypes.instanceOf(Date), - is_submitting: PropTypes.bool, - is_uploading: PropTypes.bool, - me: PropTypes.number, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func.isRequired, - onChangeSpoilerText: PropTypes.func.isRequired, - onPaste: PropTypes.func.isRequired, - onPickEmoji: PropTypes.func.isRequired -}; - -export default injectIntl(ComposeForm); diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -1,114 +0,0 @@ -import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; -import EmojiPicker from 'emojione-picker'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, - people: { id: 'emoji_button.people', defaultMessage: 'People' }, - nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, - food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, - activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, - travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, - objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, - symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, - flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' } -}); - -const settings = { - imageType: 'png', - sprites: false, - imagePathPNG: '/emoji/' -}; - -const dropdownStyle = { - position: 'absolute', - right: '5px', - top: '5px' -}; - -const dropdownTriggerStyle = { - display: 'block', - fontSize: '24px', - lineHeight: '24px', - marginLeft: '2px', - width: '24px' -} - -class EmojiPickerDropdown extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.setRef = this.setRef.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - setRef (c) { - this.dropdown = c; - } - - handleChange (data) { - this.dropdown.hide(); - this.props.onPickEmoji(data); - } - - render () { - const { intl } = this.props; - - const categories = { - people: { - title: intl.formatMessage(messages.people), - emoji: 'smile', - }, - nature: { - title: intl.formatMessage(messages.nature), - emoji: 'hamster', - }, - food: { - title: intl.formatMessage(messages.food), - emoji: 'pizza', - }, - activity: { - title: intl.formatMessage(messages.activity), - emoji: 'soccer', - }, - travel: { - title: intl.formatMessage(messages.travel), - emoji: 'earth_americas', - }, - objects: { - title: intl.formatMessage(messages.objects), - emoji: 'bulb', - }, - symbols: { - title: intl.formatMessage(messages.symbols), - emoji: 'clock9', - }, - flags: { - title: intl.formatMessage(messages.flags), - emoji: 'flag_gb', - } - } - - return ( - <Dropdown ref={this.setRef} style={dropdownStyle}> - <DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={dropdownTriggerStyle}> - <img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" /> - </DropdownTrigger> - - <DropdownContent className='dropdown__left'> - <EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search={true} /> - </DropdownContent> - </Dropdown> - ); - } - -} - -EmojiPickerDropdown.propTypes = { - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired -}; - -export default injectIntl(EmojiPickerDropdown); diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx @@ -1,32 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from '../../../components/avatar'; -import IconButton from '../../../components/icon_button'; -import DisplayName from '../../../components/display_name'; -import Permalink from '../../../components/permalink'; -import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; - -class NavigationBar extends React.PureComponent { - - render () { - return ( - <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink> - - <div className='navigation-bar__profile'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> - </Permalink> - <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - </div> - </div> - ); - } - -} - -NavigationBar.propTypes = { - account: ImmutablePropTypes.map.isRequired -}; - -export default NavigationBar; diff --git a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx @@ -1,104 +0,0 @@ -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; -import IconButton from '../../../components/icon_button'; - -const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' } -}); - -const iconStyle = { - height: null, - lineHeight: '27px' -} - -class PrivacyDropdown extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - open: false - }; - this.handleToggle = this.handleToggle.bind(this); - this.handleClick = this.handleClick.bind(this); - this.onGlobalClick = this.onGlobalClick.bind(this); - this.setRef = this.setRef.bind(this); - } - - handleToggle () { - this.setState({ open: !this.state.open }); - } - - handleClick (value, e) { - e.preventDefault(); - this.setState({ open: false }); - this.props.onChange(value); - } - - onGlobalClick (e) { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } - } - - componentDidMount () { - window.addEventListener('click', this.onGlobalClick); - window.addEventListener('touchstart', this.onGlobalClick); - } - - componentWillUnmount () { - window.removeEventListener('click', this.onGlobalClick); - window.removeEventListener('touchstart', this.onGlobalClick); - } - - setRef (c) { - this.node = c; - } - - render () { - const { value, onChange, intl } = this.props; - const { open } = this.state; - - const options = [ - { icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) }, - { icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) }, - { icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) }, - { icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) } - ]; - - const valueOption = options.find(item => item.value === value); - - return ( - <div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}> - <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div> - <div className='privacy-dropdown__dropdown'> - {options.map(item => - <div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> - <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div> - <div className='privacy-dropdown__option__content'> - <strong>{item.shortText}</strong> - {item.longText} - </div> - </div> - )} - </div> - </div> - ); - } - -} - -PrivacyDropdown.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(PrivacyDropdown); diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx @@ -1,69 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from '../../../components/avatar'; -import IconButton from '../../../components/icon_button'; -import DisplayName from '../../../components/display_name'; -import emojify from '../../../emoji'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' } -}); - -class ReplyIndicator extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - this.handleAccountClick = this.handleAccountClick.bind(this); - } - - handleClick () { - this.props.onCancel(); - } - - handleAccountClick (e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - } - - render () { - const { status, intl } = this.props; - - if (!status) { - return null; - } - - const content = { __html: emojify(status.get('content')) }; - - return ( - <div className='reply-indicator'> - <div className='reply-indicator__header'> - <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> - - <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'> - <div className='reply-indicator__display-avatar'><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div> - <DisplayName account={status.get('account')} /> - </a> - </div> - - <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> - </div> - ); - } - -} - -ReplyIndicator.contextTypes = { - router: PropTypes.object -}; - -ReplyIndicator.propTypes = { - status: ImmutablePropTypes.map, - onCancel: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(ReplyIndicator); diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -1,82 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' } -}); - -class Search extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleChange = this.handleChange.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleFocus = this.handleFocus.bind(this); - this.handleClear = this.handleClear.bind(this); - } - - handleChange (e) { - this.props.onChange(e.target.value); - } - - handleClear (e) { - e.preventDefault(); - - if (this.props.value.length > 0 || this.props.submitted) { - this.props.onClear(); - } - } - - handleKeyDown (e) { - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onSubmit(); - } - } - - noop () { - - } - - handleFocus () { - this.props.onShow(); - } - - render () { - const { intl, value, submitted } = this.props; - const hasValue = value.length > 0 || submitted; - - return ( - <div className='search'> - <input - className='search__input' - type='text' - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyDown} - onFocus={this.handleFocus} - /> - - <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> - <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> - </div> - </div> - ); - } - -} - -Search.propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(Search); diff --git a/app/assets/javascripts/components/features/compose/components/search_results.jsx b/app/assets/javascripts/components/features/compose/components/search_results.jsx @@ -1,65 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../containers/status_container'; -import { Link } from 'react-router'; - -class SearchResults extends React.PureComponent { - - render () { - const { results } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( - <div className='search-results__section'> - {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} - </div> - ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( - <div className='search-results__section'> - {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} - </div> - ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( - <div className='search-results__section'> - {results.get('hashtags').map(hashtag => - <Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> - #{hashtag} - </Link> - )} - </div> - ); - } - - return ( - <div className='search-results'> - <div className='search-results__header'> - <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> - </div> - - {accounts} - {statuses} - {hashtags} - </div> - ); - } - -} - -SearchResults.propTypes = { - results: ImmutablePropTypes.map.isRequired -}; - -export default SearchResults; diff --git a/app/assets/javascripts/components/features/compose/components/text_icon_button.jsx b/app/assets/javascripts/components/features/compose/components/text_icon_button.jsx @@ -1,35 +0,0 @@ -import PropTypes from 'prop-types'; - -class TextIconButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - e.preventDefault(); - this.props.onClick(); - } - - render () { - const { label, title, active, ariaControls } = this.props; - - return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> - {label} - </button> - ); - } - -} - -TextIconButton.propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string -}; - -export default TextIconButton; diff --git a/app/assets/javascripts/components/features/compose/components/upload_button.jsx b/app/assets/javascripts/components/features/compose/components/upload_button.jsx @@ -1,60 +0,0 @@ -import IconButton from '../../../components/icon_button'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media' } -}); - - -const iconStyle = { - height: null, - lineHeight: '27px' -} - -class UploadButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleChange = this.handleChange.bind(this); - this.handleClick = this.handleClick.bind(this); - this.setRef = this.setRef.bind(this); - } - - handleChange (e) { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - handleClick () { - this.fileElement.click(); - } - - setRef (c) { - this.fileElement = c; - } - - render () { - - const { intl, resetFileKey, disabled } = this.props; - - return ( - <div className='compose-form__upload-button'> - <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle}/> - <input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} /> - </div> - ); - } - -} - -UploadButton.propTypes = { - disabled: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - style: PropTypes.object, - resetFileKey: PropTypes.number, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(UploadButton); diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx @@ -1,45 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from '../../../components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import UploadProgressContainer from '../containers/upload_progress_container'; -import { Motion, spring } from 'react-motion'; - -const messages = defineMessages({ - undo: { id: 'upload_form.undo', defaultMessage: 'Undo' } -}); - -class UploadForm extends React.PureComponent { - - render () { - const { intl, media } = this.props; - - const uploads = media.map(attachment => - <div className='compose-form__upload' key={attachment.get('id')}> - <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> - {({ scale }) => - <div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${attachment.get('preview_url')})` }}> - <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> - </div> - } - </Motion> - </div> - ); - - return ( - <div className='compose-form__upload-wrapper'> - <UploadProgressContainer /> - <div className='compose-form__uploads-wrapper'>{uploads}</div> - </div> - ); - } - -} - -UploadForm.propTypes = { - media: ImmutablePropTypes.list.isRequired, - onRemoveFile: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(UploadForm); diff --git a/app/assets/javascripts/components/features/compose/components/upload_progress.jsx b/app/assets/javascripts/components/features/compose/components/upload_progress.jsx @@ -1,42 +0,0 @@ -import PropTypes from 'prop-types'; -import { Motion, spring } from 'react-motion'; -import { FormattedMessage } from 'react-intl'; - -class UploadProgress extends React.PureComponent { - - render () { - const { active, progress } = this.props; - - if (!active) { - return null; - } - - return ( - <div className='upload-progress'> - <div className='upload-progress__icon'> - <i className='fa fa-upload' /> - </div> - - <div className='upload-progress__message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> - - <div className='upload-progress__backdrop'> - <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> - {({ width }) => - <div className='upload-progress__tracker' style={{ width: `${width}%` }} /> - } - </Motion> - </div> - </div> - </div> - ); - } - -} - -UploadProgress.propTypes = { - active: PropTypes.bool, - progress: PropTypes.number -}; - -export default UploadProgress; diff --git a/app/assets/javascripts/components/features/compose/components/warning.jsx b/app/assets/javascripts/components/features/compose/components/warning.jsx @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; - -class Warning extends React.PureComponent { - - constructor (props) { - super(props); - } - - render () { - const { message } = this.props; - - return ( - <div className='compose-form__warning'> - {message} - </div> - ); - } - -} - -Warning.propTypes = { - message: PropTypes.node.isRequired -}; - -export default Warning; diff --git a/app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx b/app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx @@ -1,50 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import TextIconButton from '../components/text_icon_button'; -import { changeComposeSensitivity } from '../../../actions/compose'; -import { Motion, spring } from 'react-motion'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' } -}); - -const mapStateToProps = state => ({ - visible: state.getIn(['compose', 'media_attachments']).size > 0, - active: state.getIn(['compose', 'sensitive']) -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSensitivity()); - } - -}); - -class SensitiveButton extends React.PureComponent { - - render () { - const { visible, active, onClick, intl } = this.props; - - return ( - <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> - {({ scale }) => - <div style={{ display: visible ? 'block' : 'none', transform: `translateZ(0) scale(${scale})` }}> - <TextIconButton onClick={onClick} label='NSFW' title={intl.formatMessage(messages.title)} active={active} /> - </div> - } - </Motion> - ); - } - -} - -SensitiveButton.propTypes = { - visible: PropTypes.bool, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/assets/javascripts/components/features/compose/containers/warning_container.jsx b/app/assets/javascripts/components/features/compose/containers/warning_container.jsx @@ -1,48 +0,0 @@ -import { connect } from 'react-redux'; -import Warning from '../components/warning'; -import { createSelector } from 'reselect'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig)); - -const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => { - return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []; -}); - -const mapStateToProps = state => { - const mentionedUsernames = getMentionedUsernames(state); - const mentionedUsernamesWithDomains = getMentionedDomains(state); - - return { - needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null, - mentionedDomains: mentionedUsernamesWithDomains, - needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']) - }; -}; - -const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => { - if (needsLockWarning) { - return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; - } else if (needsLeakWarning) { - return ( - <Warning - message={<FormattedMessage - id='compose_form.privacy_disclaimer' - defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.' - values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }} - />} - /> - ); - } - - return null; -}; - -WarningWrapper.propTypes = { - needsLeakWarning: PropTypes.bool, - needsLockWarning: PropTypes.bool, - mentionedDomains: PropTypes.array.isRequired, -}; - -export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx @@ -1,85 +0,0 @@ -import ComposeFormContainer from './containers/compose_form_container'; -import UploadFormContainer from './containers/upload_form_container'; -import NavigationContainer from './containers/navigation_container'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { mountCompose, unmountCompose } from '../../actions/compose'; -import { Link } from 'react-router'; -import { injectIntl, defineMessages } from 'react-intl'; -import SearchContainer from './containers/search_container'; -import { Motion, spring } from 'react-motion'; -import SearchResultsContainer from './containers/search_results_container'; - -const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } -}); - -const mapStateToProps = state => ({ - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) -}); - -class Compose extends React.PureComponent { - - componentDidMount () { - this.props.dispatch(mountCompose()); - } - - componentWillUnmount () { - this.props.dispatch(unmountCompose()); - } - - render () { - const { withHeader, showSearch, intl } = this.props; - - let header = ''; - - if (withHeader) { - header = ( - <div className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role="img" aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link> - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role="img" aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role="img" aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> - <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role="img" aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role="img" aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a> - </div> - ); - } - - return ( - <div className='drawer'> - {header} - - <SearchContainer /> - - <div className='drawer__pager'> - <div className='drawer__inner'> - <NavigationContainer /> - <ComposeFormContainer /> - </div> - - <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> - {({ x }) => - <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> - <SearchResultsContainer /> - </div> - } - </Motion> - </div> - </div> - ); - } - -} - -Compose.propTypes = { - dispatch: PropTypes.func.isRequired, - withHeader: PropTypes.bool, - showSearch: PropTypes.bool, - intl: PropTypes.object.isRequired -}; - -export default connect(mapStateToProps)(injectIntl(Compose)); diff --git a/app/assets/javascripts/components/features/favourited_statuses/index.jsx b/app/assets/javascripts/components/features/favourited_statuses/index.jsx @@ -1,66 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -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 ColumnBackButtonSlim from '../../components/column_back_button_slim'; -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']) -}); - -class Favourites extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScrollToBottom = this.handleScrollToBottom.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchFavouritedStatuses()); - } - - handleScrollToBottom () { - this.props.dispatch(expandFavouritedStatuses()); - } - - render () { - const { statusIds, loaded, intl, me } = this.props; - - if (!loaded) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column icon='star' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <StatusList {...this.props} onScrollToBottom={this.handleScrollToBottom} /> - </Column> - ); - } - -} - -Favourites.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - loaded: PropTypes.bool, - intl: PropTypes.object.isRequired, - me: PropTypes.number.isRequired -}; - -export default connect(mapStateToProps)(injectIntl(Favourites)); diff --git a/app/assets/javascripts/components/features/favourites/index.jsx b/app/assets/javascripts/components/features/favourites/index.jsx @@ -1,59 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import { fetchFavourites } from '../../actions/interactions'; -import { ScrollContainer } from 'react-router-scroll'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ColumnBackButton from '../../components/column_back_button'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]) -}); - -class Favourites extends React.PureComponent { - - componentWillMount () { - this.props.dispatch(fetchFavourites(Number(this.props.params.statusId))); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId))); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='favourites'> - <div className='scrollable'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Favourites.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list -}; - -export default connect(mapStateToProps)(Favourites); diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from '../../../components/permalink'; -import Avatar from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; -import emojify from '../../../emoji'; -import IconButton from '../../../components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' } -}); - -const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => { - const content = { __html: emojify(account.get('note')) }; - - return ( - <div className='account-authorize__wrapper'> - <div className='account-authorize'> - <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'> - <div className='account-authorize__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__header__content' dangerouslySetInnerHTML={content} /> - </div> - - <div className='account--panel'> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div> - </div> - </div> - ) -}; - -AccountAuthorize.propTypes = { - account: ImmutablePropTypes.map.isRequired, - onAuthorize: PropTypes.func.isRequired, - onReject: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(AccountAuthorize); diff --git a/app/assets/javascripts/components/features/follow_requests/index.jsx b/app/assets/javascripts/components/features/follow_requests/index.jsx @@ -1,72 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -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 AccountAuthorizeContainer from './containers/account_authorize_container'; -import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' } -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'follow_requests', 'items']) -}); - -class FollowRequests extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchFollowRequests()); - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowRequests()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column icon='users' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='follow_requests'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountAuthorizeContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } -} - -FollowRequests.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired -}; - -export default connect(mapStateToProps)(injectIntl(FollowRequests)); diff --git a/app/assets/javascripts/components/features/followers/index.jsx b/app/assets/javascripts/components/features/followers/index.jsx @@ -1,90 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import { - fetchAccount, - fetchFollowers, - expandFollowers -} from '../../actions/accounts'; -import { ScrollContainer } from 'react-router-scroll'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import HeaderContainer from '../account_timeline/containers/header_container'; -import LoadMore from '../../components/load_more'; -import ColumnBackButton from '../../components/column_back_button'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']) -}); - -class Followers extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - this.handleLoadMore = this.handleLoadMore.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchFollowers(Number(this.props.params.accountId))); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId))); - } - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); - } - } - - handleLoadMore (e) { - e.preventDefault(); - this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='followers'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='followers'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - <LoadMore onClick={this.handleLoadMore} /> - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Followers.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list -}; - -export default connect(mapStateToProps)(Followers); diff --git a/app/assets/javascripts/components/features/following/index.jsx b/app/assets/javascripts/components/features/following/index.jsx @@ -1,90 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import { - fetchAccount, - fetchFollowing, - expandFollowing -} from '../../actions/accounts'; -import { ScrollContainer } from 'react-router-scroll'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import HeaderContainer from '../account_timeline/containers/header_container'; -import LoadMore from '../../components/load_more'; -import ColumnBackButton from '../../components/column_back_button'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']) -}); - -class Following extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - this.handleLoadMore = this.handleLoadMore.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchFollowing(Number(this.props.params.accountId))); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId))); - } - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); - } - } - - handleLoadMore (e) { - e.preventDefault(); - this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='following'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='following'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - <LoadMore onClick={this.handleLoadMore} /> - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Following.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list -}; - -export default connect(mapStateToProps)(Following); diff --git a/app/assets/javascripts/components/features/generic_not_found/index.jsx b/app/assets/javascripts/components/features/generic_not_found/index.jsx @@ -1,10 +0,0 @@ -import Column from '../ui/components/column'; -import MissingIndicator from '../../components/missing_indicator'; - -const GenericNotFound = () => ( - <Column> - <MissingIndicator /> - </Column> -); - -export default GenericNotFound; diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -1,66 +0,0 @@ -import Column from '../ui/components/column'; -import ColumnLink from '../ui/components/column_link'; -import ColumnSubheading from '../ui/components/column_subheading'; -import { Link } from 'react-router'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const messages = defineMessages({ - heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation'}, - settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings'}, - community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local 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: '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' } -}); - -const mapStateToProps = state => ({ - me: state.getIn(['accounts', state.getIn(['meta', 'me'])]) -}); - -const GettingStarted = ({ intl, me }) => { - let followRequests = ''; - - if (me.get('locked')) { - followRequests = <ColumnLink icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />; - } - - return ( - <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile={true}> - <div className='getting-started__wrapper'> - <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)}/> - <ColumnLink icon='users' hideOnMobile={true} text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' /> - <ColumnLink icon='globe' hideOnMobile={true} text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' /> - <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> - {followRequests} - <ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' /> - <ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' /> - <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)}/> - <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> - <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> - </div> - - <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}> - <div className='static-content getting-started'> - <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p> - </div> - </div> - </Column> - ); -}; - -GettingStarted.propTypes = { - intl: PropTypes.object.isRequired, - me: ImmutablePropTypes.map.isRequired -}; - -export default connect(mapStateToProps)(injectIntl(GettingStarted)); diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx @@ -1,89 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; -import Column from '../ui/components/column'; -import { - refreshTimeline, - updateTimeline, - deleteFromTimelines -} from '../../actions/timelines'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; -import { FormattedMessage } from 'react-intl'; -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']) -}); - -class HashtagTimeline extends React.PureComponent { - - _subscribe (dispatch, id) { - const { streamingAPIBaseURL, accessToken } = this.props; - - this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, { - - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('tag', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } - } - - }); - } - - _unsubscribe () { - if (typeof this.subscription !== 'undefined') { - this.subscription.close(); - this.subscription = null; - } - } - - componentDidMount () { - const { dispatch } = this.props; - const { id } = this.props.params; - - dispatch(refreshTimeline('tag', id)); - this._subscribe(dispatch, id); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(refreshTimeline('tag', nextProps.params.id)); - this._unsubscribe(); - this._subscribe(this.props.dispatch, nextProps.params.id); - } - } - - componentWillUnmount () { - this._unsubscribe(); - } - - render () { - const { id, hasUnread } = this.props.params; - - return ( - <Column icon='hashtag' active={hasUnread} heading={id}> - <ColumnBackButtonSlim /> - <StatusListContainer scrollKey='hashtag_timeline' type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> - </Column> - ); - } - -} - -HashtagTimeline.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - streamingAPIBaseURL: PropTypes.string.isRequired, - accessToken: PropTypes.string.isRequired, - hasUnread: PropTypes.bool -}; - -export default connect(mapStateToProps)(HashtagTimeline); diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnCollapsable from '../../../components/column_collapsable'; -import SettingToggle from '../../notifications/components/setting_toggle'; -import SettingText from './setting_text'; - -const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, - settings: { id: 'home.settings', defaultMessage: 'Column settings' } -}); - -class ColumnSettings extends React.PureComponent { - - render () { - const { settings, onChange, onSave, intl } = this.props; - - return ( - <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}> - <div className='column-settings__outer'> - <span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} /> - </div> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> - - <div className='column-settings__row'> - <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> - </div> - </div> - </ColumnCollapsable> - ); - } - -} - -ColumnSettings.propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -} - -export default injectIntl(ColumnSettings); diff --git a/app/assets/javascripts/components/features/home_timeline/components/setting_text.jsx b/app/assets/javascripts/components/features/home_timeline/components/setting_text.jsx @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -class SettingText extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleChange = this.handleChange.bind(this); - } - - handleChange (e) { - this.props.onChange(this.props.settingKey, e.target.value) - } - - render () { - const { settings, settingKey, label } = this.props; - - return ( - <input - className='setting-text' - value={settings.getIn(settingKey)} - onChange={this.handleChange} - placeholder={label} - /> - ); - } - -} - -SettingText.propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired -}; - -export default SettingText; diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx @@ -1,37 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; -import Column from '../ui/components/column'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { Link } from 'react-router'; - -const messages = defineMessages({ - title: { id: 'column.home', defaultMessage: 'Home' } -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0 -}); - -class HomeTimeline extends React.PureComponent { - - render () { - const { intl, hasUnread } = this.props; - - return ( - <Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}> - <ColumnSettingsContainer /> - <StatusListContainer {...this.props} scrollKey='home_timeline' type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} /> - </Column> - ); - } - -} - -HomeTimeline.propTypes = { - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool -}; - -export default connect(mapStateToProps)(injectIntl(HomeTimeline)); diff --git a/app/assets/javascripts/components/features/mutes/index.jsx b/app/assets/javascripts/components/features/mutes/index.jsx @@ -1,73 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -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']) -}); - -class Mutes extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - } - - 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 ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes'> - <div className='scrollable mutes' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Mutes.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired -}; - -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 @@ -1,26 +0,0 @@ -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } -}); - -class ClearColumnButton extends React.Component { - - render () { - const { intl } = this.props; - - return ( - <div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}> - <i className='fa fa-eraser' /> - </div> - ); - } -} - -ClearColumnButton.propTypes = { - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(ClearColumnButton); diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnCollapsable from '../../../components/column_collapsable'; -import SettingToggle from './setting_toggle'; - -const messages = defineMessages({ - settings: { id: 'notifications.settings', defaultMessage: 'Column settings' } -}); - -class ColumnSettings extends React.PureComponent { - - render () { - const { settings, intl, onChange, onSave } = this.props; - - const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; - const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; - const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; - - return ( - <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}> - <div className='column-settings__outer'> - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> - <SettingToggle settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> - <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> - <SettingToggle settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> - <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> - <SettingToggle settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> - <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> - - <div className='column-settings__row'> - <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> - <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> - <SettingToggle settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> - </div> - </div> - </ColumnCollapsable> - ); - } - -} - -ColumnSettings.propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, - intl: PropTypes.shape({ - formatMessage: PropTypes.func.isRequired - }).isRequired -}; - -export default injectIntl(ColumnSettings); diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -1,88 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import StatusContainer from '../../../containers/status_container'; -import AccountContainer from '../../../containers/account_container'; -import { FormattedMessage } from 'react-intl'; -import Permalink from '../../../components/permalink'; -import emojify from '../../../emoji'; -import escapeTextContentForBrowser from 'escape-html'; - -class Notification extends React.PureComponent { - - renderFollow (account, link) { - return ( - <div className='notification notification-follow'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> - </div> - - <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> - </div> - - <AccountContainer id={account.get('id')} withNote={false} /> - </div> - ); - } - - renderMention (notification) { - return <StatusContainer id={notification.get('status')} />; - } - - renderFavourite (notification, link) { - return ( - <div className='notification notification-favourite'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-star star-icon'/> - </div> - - <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> - </div> - - <StatusContainer id={notification.get('status')} muted={true} /> - </div> - ); - } - - renderReblog (notification, link) { - return ( - <div className='notification notification-reblog'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-retweet' /> - </div> - - <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} /> - </div> - - <StatusContainer id={notification.get('status')} muted={true} /> - </div> - ); - } - - 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'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; - - switch(notification.get('type')) { - case 'follow': - return this.renderFollow(account, link); - case 'mention': - return this.renderMention(notification); - case 'favourite': - return this.renderFavourite(notification, link); - case 'reblog': - return this.renderReblog(notification, link); - } - } - -} - -Notification.propTypes = { - notification: ImmutablePropTypes.map.isRequired -}; - -export default Notification; diff --git a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => ( - <label htmlFor={htmlFor} className='setting-toggle__label'> - <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} /> - <span className='setting-toggle'>{label}</span> - </label> -); - -SettingToggle.propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.node.isRequired, - onChange: PropTypes.func.isRequired, - htmlFor: PropTypes.string -}; - -export default SettingToggle; diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx @@ -1,142 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Column from '../ui/components/column'; -import { expandNotifications, clearNotifications, scrollTopNotifications } from '../../actions/notifications'; -import NotificationContainer from './containers/notification_container'; -import { ScrollContainer } from 'react-router-scroll'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { createSelector } from 'reselect'; -import Immutable from 'immutable'; -import LoadMore from '../../components/load_more'; -import ClearColumnButton from './components/clear_column_button'; -import { openModal } from '../../actions/modal'; - -const messages = defineMessages({ - title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, - clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } -}); - -const getNotifications = createSelector([ - state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), - state => state.getIn(['notifications', 'items']) -], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); - -const mapStateToProps = state => ({ - notifications: getNotifications(state), - isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0 -}); - -class Notifications extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - this.handleLoadMore = this.handleLoadMore.bind(this); - this.handleClear = this.handleClear.bind(this); - this.setRef = this.setRef.bind(this); - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - const offset = scrollHeight - scrollTop - clientHeight; - this._oldScrollPosition = scrollHeight - scrollTop; - - if (250 > offset && !this.props.isLoading) { - this.props.dispatch(expandNotifications()); - } else if (scrollTop < 100) { - this.props.dispatch(scrollTopNotifications(true)); - } else { - this.props.dispatch(scrollTopNotifications(false)); - } - } - - componentDidUpdate (prevProps) { - if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) { - this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition; - } - } - - handleLoadMore (e) { - e.preventDefault(); - this.props.dispatch(expandNotifications()); - } - - handleClear () { - const { dispatch, intl } = this.props; - - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()) - })); - } - - setRef (c) { - this.node = c; - } - - render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread } = this.props; - - let loadMore = ''; - let scrollableArea = ''; - let unread = ''; - - if (!isLoading && notifications.size > 0) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - if (isUnread) { - unread = <div className='notifications__unread-indicator' />; - } - - if (isLoading || notifications.size > 0) { - scrollableArea = ( - <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}> - {unread} - - <div> - {notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)} - {loadMore} - </div> - </div> - ); - } else { - scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." /> - </div> - ); - } - - return ( - <Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}> - <ColumnSettingsContainer /> - <ClearColumnButton onClick={this.handleClear} /> - <ScrollContainer scrollKey='notifications' shouldUpdateScroll={shouldUpdateScroll}> - {scrollableArea} - </ScrollContainer> - </Column> - ); - } - -} - -Notifications.propTypes = { - notifications: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - shouldUpdateScroll: PropTypes.func, - intl: PropTypes.object.isRequired, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool -}; - -Notifications.defaultProps = { - trackScroll: true -}; - -export default connect(mapStateToProps)(injectIntl(Notifications)); diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -1,95 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; -import Column from '../ui/components/column'; -import { - refreshTimeline, - updateTimeline, - deleteFromTimelines, - connectTimeline, - disconnectTimeline -} from '../../actions/timelines'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; -import createStream from '../../stream'; - -const messages = defineMessages({ - title: { id: 'column.public', defaultMessage: 'Federated timeline' } -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, - streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), - accessToken: state.getIn(['meta', 'access_token']) -}); - -let subscription; - -class PublicTimeline extends React.PureComponent { - - componentDidMount () { - const { dispatch, streamingAPIBaseURL, accessToken } = this.props; - - dispatch(refreshTimeline('public')); - - if (typeof subscription !== 'undefined') { - return; - } - - subscription = createStream(streamingAPIBaseURL, accessToken, 'public', { - - connected () { - dispatch(connectTimeline('public')); - }, - - reconnected () { - dispatch(connectTimeline('public')); - }, - - disconnected () { - dispatch(disconnectTimeline('public')); - }, - - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('public', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } - } - - }); - } - - componentWillUnmount () { - // if (typeof subscription !== 'undefined') { - // subscription.close(); - // subscription = null; - // } - } - - render () { - const { intl, hasUnread } = this.props; - - return ( - <Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}> - <ColumnBackButtonSlim /> - <StatusListContainer {...this.props} type='public' scrollKey='public_timeline' emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> - </Column> - ); - } - -} - -PublicTimeline.propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - streamingAPIBaseURL: PropTypes.string.isRequired, - accessToken: PropTypes.string.isRequired, - hasUnread: PropTypes.bool -}; - -export default connect(mapStateToProps)(injectIntl(PublicTimeline)); diff --git a/app/assets/javascripts/components/features/reblogs/index.jsx b/app/assets/javascripts/components/features/reblogs/index.jsx @@ -1,59 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import { fetchReblogs } from '../../actions/interactions'; -import { ScrollContainer } from 'react-router-scroll'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ColumnBackButton from '../../components/column_back_button'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]) -}); - -class Reblogs extends React.PureComponent { - - componentWillMount () { - this.props.dispatch(fetchReblogs(Number(this.props.params.statusId))); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchReblogs(Number(nextProps.params.statusId))); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='reblogs'> - <div className='scrollable reblogs'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Reblogs.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list -}; - -export default connect(mapStateToProps)(Reblogs); diff --git a/app/assets/javascripts/components/features/report/components/status_check_box.jsx b/app/assets/javascripts/components/features/report/components/status_check_box.jsx @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import emojify from '../../../emoji'; -import Toggle from 'react-toggle'; - -class StatusCheckBox extends React.PureComponent { - - render () { - const { status, checked, onToggle, disabled } = this.props; - const content = { __html: emojify(status.get('content')) }; - - if (status.get('reblog')) { - return null; - } - - return ( - <div className='status-check-box'> - <div - className='status__content' - dangerouslySetInnerHTML={content} - /> - - <div className='status-check-box-toggle'> - <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> - </div> - </div> - ); - } - -} - -StatusCheckBox.propTypes = { - status: ImmutablePropTypes.map.isRequired, - checked: PropTypes.bool, - onToggle: PropTypes.func.isRequired, - disabled: PropTypes.bool -}; - -export default StatusCheckBox; diff --git a/app/assets/javascripts/components/features/report/index.jsx b/app/assets/javascripts/components/features/report/index.jsx @@ -1,130 +0,0 @@ -import { connect } from 'react-redux'; -import { cancelReport, changeReportComment, submitReport } from '../../actions/reports'; -import { fetchAccountTimeline } from '../../actions/accounts'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Column from '../ui/components/column'; -import Button from '../../components/button'; -import { makeGetAccount } from '../../selectors'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import StatusCheckBox from './containers/status_check_box_container'; -import Immutable from 'immutable'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; - -const messages = defineMessages({ - heading: { id: 'report.heading', defaultMessage: 'New report' }, - placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, - submit: { id: 'report.submit', defaultMessage: 'Submit' } -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = state => { - const accountId = state.getIn(['reports', 'new', 'account_id']); - - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: getAccount(state, accountId), - comment: state.getIn(['reports', 'new', 'comment']), - statusIds: Immutable.OrderedSet(state.getIn(['timelines', 'accounts_timelines', accountId, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])) - }; - }; - - return mapStateToProps; -}; - -class Report extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleCommentChange = this.handleCommentChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - componentWillMount () { - if (!this.props.account) { - this.context.router.replace('/'); - } - } - - componentDidMount () { - if (!this.props.account) { - return; - } - - this.props.dispatch(fetchAccountTimeline(this.props.account.get('id'))); - } - - componentWillReceiveProps (nextProps) { - if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id'))); - } - } - - handleCommentChange (e) { - this.props.dispatch(changeReportComment(e.target.value)); - } - - handleSubmit () { - this.props.dispatch(submitReport()); - this.context.router.replace('/'); - } - - render () { - const { account, comment, intl, statusIds, isSubmitting } = this.props; - - if (!account) { - return null; - } - - return ( - <Column heading={intl.formatMessage(messages.heading)} icon='flag'> - <ColumnBackButtonSlim /> - - <div className='report scrollable'> - <div className='report__target'> - <FormattedMessage id='report.target' defaultMessage='Reporting' /> - <strong>{account.get('acct')}</strong> - </div> - - <div className='scrollable report__statuses'> - <div> - {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} - </div> - </div> - - <div className='report__textarea-wrapper'> - <textarea - className='report__textarea' - placeholder={intl.formatMessage(messages.placeholder)} - value={comment} - onChange={this.handleCommentChange} - disabled={isSubmitting} - /> - - <div className='report__submit'> - <div className='report__submit-button'><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div> - </div> - </div> - </div> - </Column> - ); - } - -} - -Report.contextTypes = { - router: PropTypes.object -}; - -Report.propTypes = { - isSubmitting: PropTypes.bool, - account: ImmutablePropTypes.map, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - comment: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default connect(makeMapStateToProps)(injectIntl(Report)); diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import IconButton from '../../../components/icon_button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import DropdownMenu from '../../../components/dropdown_menu'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' } -}); - -class ActionBar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleReplyClick = this.handleReplyClick.bind(this); - this.handleReblogClick = this.handleReblogClick.bind(this); - this.handleFavouriteClick = this.handleFavouriteClick.bind(this); - this.handleDeleteClick = this.handleDeleteClick.bind(this); - this.handleMentionClick = this.handleMentionClick.bind(this); - this.handleReport = this.handleReport.bind(this); - } - - handleReplyClick () { - this.props.onReply(this.props.status); - } - - handleReblogClick (e) { - this.props.onReblog(this.props.status, e); - } - - handleFavouriteClick () { - this.props.onFavourite(this.props.status); - } - - handleDeleteClick () { - this.props.onDelete(this.props.status); - } - - handleMentionClick () { - this.props.onMention(this.props.status.get('account'), this.context.router); - } - - handleReport () { - this.props.onReport(this.props.status); - this.context.router.push('/report'); - } - - render () { - const { status, me, intl } = this.props; - - let menu = []; - - if (me === status.getIn(['account', 'id'])) { - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - 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'; - - let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); - - return ( - <div className='detailed-status__action-bar'> - <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> - <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> - <div className='detailed-status__button'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> - <div className='detailed-status__button'><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" ariaLabel="More" /></div> - </div> - ); - } - -} - -ActionBar.contextTypes = { - router: PropTypes.object -}; - -ActionBar.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func.isRequired, - onReblog: PropTypes.func.isRequired, - onFavourite: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func, - me: PropTypes.number.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(ActionBar); diff --git a/app/assets/javascripts/components/features/status/components/card.jsx b/app/assets/javascripts/components/features/status/components/card.jsx @@ -1,95 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const hostStyle = { - display: 'block', - marginTop: '5px', - fontSize: '13px' -}; - -const getHostname = url => { - const parser = document.createElement('a'); - parser.href = url; - return parser.hostname; -}; - -class Card extends React.PureComponent { - - renderLink () { - const { card } = this.props; - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( - <div className='status-card__image'> - <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' /> - </div> - ); - } - - if (provider.length < 1) { - provider = getHostname(card.get('url')) - } - - return ( - <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'> - {image} - - <div className='status-card__content'> - <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - <p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p> - <span className='status-card__host' style={hostStyle}>{provider}</span> - </div> - </a> - ); - } - - renderPhoto () { - const { card } = this.props; - - return ( - <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> - <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> - </a> - ); - } - - renderVideo () { - const { card } = this.props; - const content = { __html: card.get('html') }; - - return ( - <div - className='status-card-video' - dangerouslySetInnerHTML={content} - /> - ); - } - - render () { - const { card } = this.props; - - if (card === null) { - return null; - } - - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; - } - } -} - -Card.propTypes = { - card: ImmutablePropTypes.map -}; - -export default Card; diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -1,94 +0,0 @@ -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; -import StatusContent from '../../../components/status_content'; -import MediaGallery from '../../../components/media_gallery'; -import VideoPlayer from '../../../components/video_player'; -import AttachmentList from '../../../components/attachment_list'; -import { Link } from 'react-router'; -import { FormattedDate, FormattedNumber } from 'react-intl'; -import CardContainer from '../containers/card_container'; - -class DetailedStatus extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleAccountClick = this.handleAccountClick.bind(this); - } - - handleAccountClick (e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - e.stopPropagation(); - } - - render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; - - let media = ''; - let applicationLink = ''; - - if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media = <AttachmentList media={status.get('media_attachments')} />; - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />; - } else { - media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />; - } - } else if (status.get('spoiler_text').length === 0) { - media = <CardContainer statusId={status.get('id')} />; - } - - if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; - } - - return ( - <div className='detailed-status'> - <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> - <div className='detailed-status__display-avatar'><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> - <DisplayName account={status.get('account')} /> - </a> - - <StatusContent status={status} /> - - {media} - - <div className='detailed-status__meta'> - <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> - <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> - </a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <i className='fa fa-retweet' /> - <span className='detailed-status__reblogs'> - <FormattedNumber value={status.get('reblogs_count')} /> - </span> - </Link> · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <i className='fa fa-star' /> - <span className='detailed-status__favorites'> - <FormattedNumber value={status.get('favourites_count')} /> - </span> - </Link> - </div> - </div> - ); - } - -} - -DetailedStatus.contextTypes = { - router: PropTypes.object -}; - -DetailedStatus.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onOpenMedia: PropTypes.func.isRequired, - onOpenVideo: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool, -}; - -export default DetailedStatus; diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx @@ -1,197 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchStatus } from '../../actions/statuses'; -import Immutable from 'immutable'; -import EmbeddedStatus from '../../components/status'; -import MissingIndicator from '../../components/missing_indicator'; -import DetailedStatus from './components/detailed_status'; -import ActionBar from './components/action_bar'; -import Column from '../ui/components/column'; -import { - favourite, - unfavourite, - reblog, - unreblog -} from '../../actions/interactions'; -import { - replyCompose, - mentionCompose -} from '../../actions/compose'; -import { deleteStatus } from '../../actions/statuses'; -import { initReport } from '../../actions/reports'; -import { - makeGetStatus, - getStatusAncestors, - getStatusDescendants -} from '../../selectors'; -import { ScrollContainer } from 'react-router-scroll'; -import ColumnBackButton from '../../components/column_back_button'; -import StatusContainer from '../../containers/status_container'; -import { openModal } from '../../actions/modal'; -import { isMobile } from '../../is_mobile' -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' } -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, Number(props.params.statusId)), - ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), - descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), - me: state.getIn(['meta', 'me']), - boostModal: state.getIn(['meta', 'boost_modal']), - autoPlayGif: state.getIn(['meta', 'auto_play_gif']) - }); - - return mapStateToProps; -}; - -class Status extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleFavouriteClick = this.handleFavouriteClick.bind(this); - this.handleReplyClick = this.handleReplyClick.bind(this); - this.handleModalReblog = this.handleModalReblog.bind(this); - this.handleReblogClick = this.handleReblogClick.bind(this); - this.handleDeleteClick = this.handleDeleteClick.bind(this); - this.handleMentionClick = this.handleMentionClick.bind(this); - this.handleOpenMedia = this.handleOpenMedia.bind(this); - this.handleOpenVideo = this.handleOpenVideo.bind(this); - this.handleReport = this.handleReport.bind(this); - } - - componentWillMount () { - this.props.dispatch(fetchStatus(Number(this.props.params.statusId))); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchStatus(Number(nextProps.params.statusId))); - } - } - - handleFavouriteClick (status) { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); - } else { - this.props.dispatch(favourite(status)); - } - } - - handleReplyClick (status) { - this.props.dispatch(replyCompose(status, this.context.router)); - } - - handleModalReblog (status) { - this.props.dispatch(reblog(status)); - } - - handleReblogClick (status, e) { - if (status.get('reblogged')) { - this.props.dispatch(unreblog(status)); - } else { - if (e.shiftKey || !this.props.boostModal) { - this.handleModalReblog(status); - } else { - this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); - } - } - } - - handleDeleteClick (status) { - const { dispatch, intl } = this.props; - - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))) - })); - } - - handleMentionClick (account, router) { - this.props.dispatch(mentionCompose(account, router)); - } - - handleOpenMedia (media, index) { - this.props.dispatch(openModal('MEDIA', { media, index })); - } - - handleOpenVideo (media, time) { - this.props.dispatch(openModal('VIDEO', { media, time })); - } - - handleReport (status) { - this.props.dispatch(initReport(status.get('account'), status)); - } - - renderChildren (list) { - return list.map(id => <StatusContainer key={id} id={id} />); - } - - render () { - let ancestors, descendants; - const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props; - - if (status === null) { - return ( - <Column> - <ColumnBackButton /> - <MissingIndicator /> - </Column> - ); - } - - const account = status.get('account'); - - if (ancestorsIds && ancestorsIds.size > 0) { - ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; - } - - if (descendantsIds && descendantsIds.size > 0) { - descendants = <div>{this.renderChildren(descendantsIds)}</div>; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='thread'> - <div className='scrollable detailed-status__wrapper'> - {ancestors} - - <DetailedStatus status={status} autoPlayGif={autoPlayGif} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} /> - <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} /> - - {descendants} - </div> - </ScrollContainer> - </Column> - ); - } - -} - -Status.contextTypes = { - router: PropTypes.object -}; - -Status.propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - status: ImmutablePropTypes.map, - ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list, - me: PropTypes.number, - boostModal: PropTypes.bool, - autoPlayGif: PropTypes.bool, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(connect(makeMapStateToProps)(Status)); diff --git a/app/assets/javascripts/components/features/ui/components/boost_modal.jsx b/app/assets/javascripts/components/features/ui/components/boost_modal.jsx @@ -1,82 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import IconButton from '../../../components/icon_button'; -import Button from '../../../components/button'; -import StatusContent from '../../../components/status_content'; -import Avatar from '../../../components/avatar'; -import RelativeTimestamp from '../../../components/relative_timestamp'; -import DisplayName from '../../../components/display_name'; - -const messages = defineMessages({ - reblog: { id: 'status.reblog', defaultMessage: 'Boost' } -}); - -class BoostModal extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleReblog = this.handleReblog.bind(this); - this.handleAccountClick = this.handleAccountClick.bind(this); - } - - handleReblog() { - this.props.onReblog(this.props.status); - this.props.onClose(); - } - - handleAccountClick (e) { - if (e.button === 0) { - e.preventDefault(); - this.props.onClose(); - this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - } - - render () { - const { status, intl, onClose } = this.props; - - return ( - <div className='modal-root__modal boost-modal'> - <div className='boost-modal__container'> - <div className='status light'> - <div className='boost-modal__status-header'> - <div className='boost-modal__status-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - - <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> - </div> - - <DisplayName account={status.get('account')} /> - </a> - </div> - - <StatusContent status={status} /> - </div> - </div> - - <div className='boost-modal__action-bar'> - <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> - <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} /> - </div> - </div> - ); - } - -} - -BoostModal.contextTypes = { - router: PropTypes.object -}; - -BoostModal.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReblog: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(BoostModal); diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx @@ -1,82 +0,0 @@ -import ColumnHeader from './column_header'; -import PropTypes from 'prop-types'; - -const easingOutQuint = (x, t, b, c, d) => c*((t=t/d-1)*t*t*t*t + 1) + b; - -const scrollTop = (node) => { - const startTime = Date.now(); - const offset = node.scrollTop; - const targetY = -offset; - const duration = 1000; - let interrupt = false; - - const step = () => { - const elapsed = Date.now() - startTime; - const percentage = elapsed / duration; - - if (percentage > 1 || interrupt) { - return; - } - - node.scrollTop = easingOutQuint(0, elapsed, offset, targetY, duration); - requestAnimationFrame(step); - }; - - step(); - - return () => { - interrupt = true; - }; -}; - -class Column extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleHeaderClick = this.handleHeaderClick.bind(this); - this.handleWheel = this.handleWheel.bind(this); - } - - handleHeaderClick () { - const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable'); - if (!scrollable) { - return; - } - this._interruptScrollAnimation = scrollTop(scrollable); - } - - handleWheel () { - if (typeof this._interruptScrollAnimation !== 'undefined') { - this._interruptScrollAnimation(); - } - } - - render () { - const { heading, icon, children, active, hideHeadingOnMobile } = this.props; - - let columnHeaderId = null - let header = ''; - - if (heading) { - columnHeaderId = heading.replace(/ /g, '-') - header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId}/>; - } - return ( - <div role='region' aria-labelledby={columnHeaderId} className='column' onWheel={this.handleWheel}> - {header} - {children} - </div> - ); - } - -} - -Column.propTypes = { - heading: PropTypes.string, - icon: PropTypes.string, - children: PropTypes.node, - active: PropTypes.bool, - hideHeadingOnMobile: PropTypes.bool -}; - -export default Column; diff --git a/app/assets/javascripts/components/features/ui/components/column_header.jsx b/app/assets/javascripts/components/features/ui/components/column_header.jsx @@ -1,42 +0,0 @@ -import PropTypes from 'prop-types' - -class ColumnHeader extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick () { - this.props.onClick(); - } - - render () { - const { type, active, hideOnMobile, columnHeaderId } = this.props; - - let icon = ''; - - if (this.props.icon) { - icon = <i className={`fa fa-fw fa-${this.props.icon} column-header__icon`} />; - } - - return ( - <div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> - {icon} - {type} - </div> - ); - } - -} - -ColumnHeader.propTypes = { - icon: PropTypes.string, - type: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func, - hideOnMobile: PropTypes.bool, - columnHeaderId: PropTypes.string -}; - -export default ColumnHeader; diff --git a/app/assets/javascripts/components/features/ui/components/column_link.jsx b/app/assets/javascripts/components/features/ui/components/column_link.jsx @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; -import { Link } from 'react-router'; - -const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => { - if (href) { - return ( - <a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </a> - ); - } else { - return ( - <Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </Link> - ); - } -}; - -ColumnLink.propTypes = { - icon: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - to: PropTypes.string, - href: PropTypes.string, - method: PropTypes.string, - hideOnMobile: PropTypes.bool -}; - -export default ColumnLink; diff --git a/app/assets/javascripts/components/features/ui/components/column_subheading.jsx b/app/assets/javascripts/components/features/ui/components/column_subheading.jsx @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -const ColumnSubheading = ({ text }) => { - return ( - <div className='column-subheading'> - {text} - </div> - ); - }; - -ColumnSubheading.propTypes = { - text: PropTypes.string.isRequired, -}; - -export default ColumnSubheading; diff --git a/app/assets/javascripts/components/features/ui/components/columns_area.jsx b/app/assets/javascripts/components/features/ui/components/columns_area.jsx @@ -1,19 +0,0 @@ -import PropTypes from 'prop-types'; - -class ColumnsArea extends React.PureComponent { - - render () { - return ( - <div className='columns-area'> - {this.props.children} - </div> - ); - } - -} - -ColumnsArea.propTypes = { - children: PropTypes.node -}; - -export default ColumnsArea; diff --git a/app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx b/app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Button from '../../../components/button'; - -class ConfirmationModal extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - this.handleCancel = this.handleCancel.bind(this); - } - - handleClick () { - this.props.onClose(); - this.props.onConfirm(); - } - - handleCancel (e) { - e.preventDefault(); - this.props.onClose(); - } - - render () { - const { intl, message, confirm, onConfirm, onClose } = this.props; - - return ( - <div className='modal-root__modal confirmation-modal'> - <div className='confirmation-modal__container'> - {message} - </div> - - <div className='confirmation-modal__action-bar'> - <div><a href='#' onClick={this.handleCancel}><FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /></a></div> - <Button text={confirm} onClick={this.handleClick} /> - </div> - </div> - ); - } - -} - -ConfirmationModal.propTypes = { - message: PropTypes.node.isRequired, - confirm: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(ConfirmationModal); diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -1,101 +0,0 @@ -import LoadingIndicator from '../../../components/loading_indicator'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; -import ImageLoader from 'react-imageloader'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from '../../../components/icon_button'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' } -}); - -class MediaModal extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - index: null - }; - this.handleNextClick = this.handleNextClick.bind(this); - this.handlePrevClick = this.handlePrevClick.bind(this); - this.handleKeyUp = this.handleKeyUp.bind(this); - } - - handleNextClick () { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); - } - - handlePrevClick () { - this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); - } - - handleKeyUp (e) { - switch(e.key) { - case 'ArrowLeft': - this.handlePrevClick(); - break; - case 'ArrowRight': - this.handleNextClick(); - break; - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getIndex () { - return this.state.index !== null ? this.state.index : this.props.index; - } - - render () { - const { media, intl, onClose } = this.props; - - const index = this.getIndex(); - const attachment = media.get(index); - const url = attachment.get('url'); - - let leftNav, rightNav, content; - - leftNav = rightNav = content = ''; - - if (media.size > 1) { - leftNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; - rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; - } - - if (attachment.get('type') === 'image') { - content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />; - } else if (attachment.get('type') === 'gifv') { - content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />; - } - - return ( - <div className='modal-root__modal media-modal'> - {leftNav} - - <div className='media-modal__content'> - <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> - {content} - </div> - - {rightNav} - </div> - ); - } - -} - -MediaModal.propTypes = { - media: ImmutablePropTypes.list.isRequired, - index: PropTypes.number.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(MediaModal); diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; -import MediaModal from './media_modal'; -import OnboardingModal from './onboarding_modal'; -import VideoModal from './video_modal'; -import BoostModal from './boost_modal'; -import ConfirmationModal from './confirmation_modal'; -import { TransitionMotion, spring } from 'react-motion'; - -const MODAL_COMPONENTS = { - 'MEDIA': MediaModal, - 'ONBOARDING': OnboardingModal, - 'VIDEO': VideoModal, - 'BOOST': BoostModal, - 'CONFIRM': ConfirmationModal -}; - -class ModalRoot extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleKeyUp = this.handleKeyUp.bind(this); - } - - handleKeyUp (e) { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.type) { - this.props.onClose(); - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - willEnter () { - return { opacity: 0, scale: 0.98 }; - } - - willLeave () { - return { opacity: spring(0), scale: spring(0.98) }; - } - - render () { - const { type, props, onClose } = this.props; - const items = []; - - if (!!type) { - items.push({ - key: type, - data: { type, props }, - style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } - }); - } - - return ( - <TransitionMotion - styles={items} - willEnter={this.willEnter} - willLeave={this.willLeave}> - {interpolatedStyles => - <div className='modal-root'> - {interpolatedStyles.map(({ key, data: { type, props }, style }) => { - const SpecificComponent = MODAL_COMPONENTS[type]; - - return ( - <div key={key}> - <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> - <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> - <SpecificComponent {...props} onClose={onClose} /> - </div> - </div> - ); - })} - </div> - } - </TransitionMotion> - ); - } - -} - -ModalRoot.propTypes = { - type: PropTypes.string, - props: PropTypes.object, - onClose: PropTypes.func.isRequired -}; - -export default ModalRoot; diff --git a/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx @@ -1,263 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Permalink from '../../../components/permalink'; -import { TransitionMotion, spring } from 'react-motion'; -import ComposeForm from '../../compose/components/compose_form'; -import Search from '../../compose/components/search'; -import NavigationBar from '../../compose/components/navigation_bar'; -import ColumnHeader from './column_header'; -import Immutable from 'immutable'; - -const messages = defineMessages({ - home_title: { id: 'column.home', defaultMessage: 'Home' }, - notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - local_title: { id: 'column.community', defaultMessage: 'Local timeline' }, - federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' } -}); - -const PageOne = ({ acct, domain }) => ( - <div className='onboarding-modal__page onboarding-modal__page-one'> - <div style={{ flex: '0 0 auto' }}> - <div className='onboarding-modal__page-one__elephant-friend' /> - </div> - - <div> - <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1> - <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' /></p> - <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p> - </div> - </div> -); - -PageOne.propTypes = { - acct: PropTypes.string.isRequired, - domain: PropTypes.string.isRequired -}; - -const PageTwo = ({ me }) => ( - <div className='onboarding-modal__page onboarding-modal__page-two'> - <div className='figure non-interactive'> - <div className='pseudo-drawer'> - <NavigationBar account={me} /> - </div> - <ComposeForm - text='Awoo! #introductions' - suggestions={Immutable.List()} - mentionedDomains={[]} - spoiler={false} - onChange={() => {}} - onSubmit={() => {}} - onPaste={() => {}} - onPickEmoji={() => {}} - onChangeSpoilerText={() => {}} - onClearSuggestions={() => {}} - onFetchSuggestions={() => {}} - onSuggestionSelected={() => {}} - /> - </div> - - <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p> - </div> -); - -PageTwo.propTypes = { - me: ImmutablePropTypes.map.isRequired, -}; - -const PageThree = ({ me, domain }) => ( - <div className='onboarding-modal__page onboarding-modal__page-three'> - <div className='figure non-interactive'> - <Search - value='' - onChange={() => {}} - onSubmit={() => {}} - onClear={() => {}} - onShow={() => {}} - /> - - <div className='pseudo-drawer'> - <NavigationBar account={me} /> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p> - <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p> - </div> -); - -PageThree.propTypes = { - me: ImmutablePropTypes.map.isRequired, - domain: PropTypes.string.isRequired -}; - -const PageFour = ({ domain, intl }) => ( - <div className='onboarding-modal__page onboarding-modal__page-four'> - <div className='onboarding-modal__page-four__columns'> - <div className='row'> - <div> - <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.'/></p> - </div> - - <div> - <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' /></p> - </div> - </div> - - <div className='row'> - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div> - </div> - - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} /></p> - </div> - </div> -); - -PageFour.propTypes = { - domain: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired -}; - -const PageSix = ({ admin, domain }) => { - let adminSection = ''; - - if (admin) { - adminSection = ( - <p> - <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} /> - <br /> - <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/> - </p> - ); - } - - return ( - <div className='onboarding-modal__page onboarding-modal__page-six'> - <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1> - {adminSection} - <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> - <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> - <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p> - </div> - ); -}; - -PageSix.propTypes = { - admin: ImmutablePropTypes.map, - domain: PropTypes.string.isRequired -}; - -const mapStateToProps = state => ({ - me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), - admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), - domain: state.getIn(['meta', 'domain']) -}); - -class OnboardingModal extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - currentIndex: 0 - }; - this.handleSkip = this.handleSkip.bind(this); - this.handleDot = this.handleDot.bind(this); - this.handleNext = this.handleNext.bind(this); - } - - handleSkip (e) { - e.preventDefault(); - this.props.onClose(); - } - - handleDot (i, e) { - e.preventDefault(); - this.setState({ currentIndex: i }); - } - - handleNext (maxNum, e) { - e.preventDefault(); - - if (this.state.currentIndex < maxNum - 1) { - this.setState({ currentIndex: this.state.currentIndex + 1 }); - } else { - this.props.onClose(); - } - } - - render () { - const { me, admin, domain, intl } = this.props; - - const pages = [ - <PageOne acct={me.get('acct')} domain={domain} />, - <PageTwo me={me} />, - <PageThree me={me} domain={domain} />, - <PageFour domain={domain} intl={intl} />, - <PageSix admin={admin} domain={domain} /> - ]; - - const { currentIndex } = this.state; - const hasMore = currentIndex < pages.length - 1; - - let nextOrDoneBtn; - - if(hasMore) { - nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>; - } else { - nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.done' defaultMessage='Done' /></a>; - } - - const styles = pages.map((page, i) => ({ - key: `page-${i}`, - style: { opacity: spring(i === currentIndex ? 1 : 0) } - })); - - return ( - <div className='modal-root__modal onboarding-modal'> - <TransitionMotion styles={styles}> - {interpolatedStyles => - <div className='onboarding-modal__pager'> - {pages.map((page, i) => - <div key={`page-${i}`} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div> - )} - </div> - } - </TransitionMotion> - - <div className='onboarding-modal__paginator'> - <div> - <a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a> - </div> - - <div className='onboarding-modal__dots'> - {pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)} - </div> - - <div> - {nextOrDoneBtn} - </div> - </div> - </div> - ); - } - -} - -OnboardingModal.propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - me: ImmutablePropTypes.map.isRequired, - domain: PropTypes.string.isRequired, - admin: ImmutablePropTypes.map -} - -export default connect(mapStateToProps)(injectIntl(OnboardingModal)); diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx @@ -1,23 +0,0 @@ -import { Link } from 'react-router'; -import { FormattedMessage } from 'react-intl'; - -class TabsBar extends React.Component { - - render () { - return ( - <div className='tabs-bar'> - <Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link> - <Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link> - <Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link> - - <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link> - <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link> - - <Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link> - </div> - ); - } - -} - -export default TabsBar; diff --git a/app/assets/javascripts/components/features/ui/components/upload_area.jsx b/app/assets/javascripts/components/features/ui/components/upload_area.jsx @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; -import { Motion, spring } from 'react-motion'; -import { FormattedMessage } from 'react-intl'; - -class UploadArea extends React.PureComponent { - - constructor (props, context) { - super(props, context); - - this.handleKeyUp = this.handleKeyUp.bind(this); - } - - handleKeyUp (e) { - e.preventDefault(); - e.stopPropagation(); - - const keyCode = e.keyCode - if (this.props.active) { - switch(keyCode) { - case 27: - this.props.onClose(); - break; - } - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - render () { - const { active } = this.props; - - return ( - <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}> - {({ backgroundOpacity, backgroundScale }) => - <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> - <div className='upload-area__drop'> - <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} /> - <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> - </div> - </div> - } - </Motion> - ); - } - -} - -UploadArea.propTypes = { - active: PropTypes.bool, - onClose: PropTypes.func -}; - -export default UploadArea; diff --git a/app/assets/javascripts/components/features/ui/components/video_modal.jsx b/app/assets/javascripts/components/features/ui/components/video_modal.jsx @@ -1,38 +0,0 @@ -import LoadingIndicator from '../../../components/loading_indicator'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from '../../../components/icon_button'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' } -}); - -class VideoModal extends React.PureComponent { - - render () { - const { media, intl, time, onClose } = this.props; - - const url = media.get('url'); - - return ( - <div className='modal-root__modal media-modal'> - <div> - <div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div> - <ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} /> - </div> - </div> - ); - } - -} - -VideoModal.propTypes = { - media: ImmutablePropTypes.map.isRequired, - time: PropTypes.number, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(VideoModal); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx @@ -1,166 +0,0 @@ -import ColumnsArea from './components/columns_area'; -import NotificationsContainer from './containers/notifications_container'; -import PropTypes from 'prop-types'; -import LoadingBarContainer from './containers/loading_bar_container'; -import HomeTimeline from '../home_timeline'; -import Compose from '../compose'; -import TabsBar from './components/tabs_bar'; -import ModalContainer from './containers/modal_container'; -import Notifications from '../notifications'; -import { connect } from 'react-redux'; -import { isMobile } from '../../is_mobile'; -import { debounce } from 'react-decoration'; -import { uploadCompose } from '../../actions/compose'; -import { refreshTimeline } from '../../actions/timelines'; -import { refreshNotifications } from '../../actions/notifications'; -import UploadArea from './components/upload_area'; - -class UI extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - width: window.innerWidth, - draggingOver: false - }; - this.handleResize = this.handleResize.bind(this); - this.handleDragEnter = this.handleDragEnter.bind(this); - this.handleDragOver = this.handleDragOver.bind(this); - this.handleDrop = this.handleDrop.bind(this); - this.handleDragLeave = this.handleDragLeave.bind(this); - this.handleDragEnd = this.handleDragLeave.bind(this) - this.closeUploadModal = this.closeUploadModal.bind(this) - this.setRef = this.setRef.bind(this); - } - - @debounce(500) - handleResize () { - this.setState({ width: window.innerWidth }); - } - - handleDragEnter (e) { - e.preventDefault(); - - if (!this.dragTargets) { - this.dragTargets = []; - } - - if (this.dragTargets.indexOf(e.target) === -1) { - this.dragTargets.push(e.target); - } - - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { - this.setState({ draggingOver: true }); - } - } - - handleDragOver (e) { - e.preventDefault(); - e.stopPropagation(); - - try { - e.dataTransfer.dropEffect = 'copy'; - } catch (err) { - - } - - return false; - } - - handleDrop (e) { - e.preventDefault(); - - this.setState({ draggingOver: false }); - - if (e.dataTransfer && e.dataTransfer.files.length === 1) { - this.props.dispatch(uploadCompose(e.dataTransfer.files)); - } - } - - handleDragLeave (e) { - e.preventDefault(); - e.stopPropagation(); - - this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); - - if (this.dragTargets.length > 0) { - return; - } - - this.setState({ draggingOver: false }); - } - - closeUploadModal() { - this.setState({ draggingOver: false }); - } - - componentWillMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - document.addEventListener('dragenter', this.handleDragEnter, false); - document.addEventListener('dragover', this.handleDragOver, false); - document.addEventListener('drop', this.handleDrop, false); - document.addEventListener('dragleave', this.handleDragLeave, false); - document.addEventListener('dragend', this.handleDragEnd, false); - - this.props.dispatch(refreshTimeline('home')); - this.props.dispatch(refreshNotifications()); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - document.removeEventListener('dragenter', this.handleDragEnter); - document.removeEventListener('dragover', this.handleDragOver); - document.removeEventListener('drop', this.handleDrop); - document.removeEventListener('dragleave', this.handleDragLeave); - document.removeEventListener('dragend', this.handleDragEnd); - } - - setRef (c) { - this.node = c; - } - - render () { - const { width, draggingOver } = this.state; - const { children } = this.props; - - let mountedColumns; - - if (isMobile(width)) { - mountedColumns = ( - <ColumnsArea> - {children} - </ColumnsArea> - ); - } else { - mountedColumns = ( - <ColumnsArea> - <Compose withHeader={true} /> - <HomeTimeline shouldUpdateScroll={() => false} /> - <Notifications shouldUpdateScroll={() => false} /> - <div style={{display: 'flex', flex: '1 1 auto', position: 'relative'}}>{children}</div> - </ColumnsArea> - ); - } - - return ( - <div className='ui' ref={this.setRef}> - <TabsBar /> - - {mountedColumns} - - <NotificationsContainer /> - <LoadingBarContainer className="loading-bar" /> - <ModalContainer /> - <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> - </div> - ); - } - -} - -UI.propTypes = { - dispatch: PropTypes.func.isRequired, - children: PropTypes.node -}; - -export default connect()(UI); diff --git a/app/assets/javascripts/components/locales/ar.jsx b/app/assets/javascripts/components/locales/ar.jsx @@ -1,131 +0,0 @@ -/** - * ملاحظة للمساهمين و المساهمات : - * لجعل مهمة المساهمين الآخرين أسهل، رجاءا تذكر : - * 1. إضافة سلسلة جديدة هنا؛ و - * 2. لإزالة السلاسل القديمة التي لم تعد هناك حاجة إليها. و - * 3. لفرز السلاسل تبعا للأبجدية - * شكر! - */ -const ar = { - "account.block": "حظر @{name}", - "account.disclaimer": "هذا المستخدم من مثيل خادم آخر. قد يكون هذا الرقم أكبر.", - "account.edit_profile": "تعديل الملف الشخصي", - "account.follow": "إتبع", - "account.followers": "المتابعون", - "account.follows": "يتبع", - "account.follows_you": "يتابعك", - "account.mention": "أُذكُر @{name}", - "account.mute": "أكتم @{name}", - "account.posts": "المشاركات", - "account.report": "أبلغ عن @{name}", - "account.requested": "في انتظار الموافقة", - "account.unblock": "إلغاء الحظر عن @{name}", - "account.unfollow": "إلغاء المتابعة", - "account.unmute": "إلغاء الكتم عن @{name}", - "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة", - "column.blocks": "الحسابات المحجوبة", - "column.community": "الخيط العام المحلي", - "column.favourites": "المفضلة", - "column.follow_requests": "طلبات المتابعة", - "column.home": "الرئيسية", - "column.mutes": "الحسابات المكتومة", - "column.notifications": "الإشعارات", - "column.public": "الخيط العام الموحد", - "column_back_button.label": "العودة", - "column_subheading.navigation": "التصفح", - "column_subheading.settings": "الإعدادات", - "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.", - "compose_form.lock_disclaimer.lock": "مقفل", - "compose_form.placeholder": "فيمَ تفكّر؟", - "compose_form.publish": "بوّق !", - "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس", - "compose_form.spoiler": "أخفِ النص واعرض تحذيرا", - "compose_form.spoiler_placeholder": "تنبيه عن المحتوى", - "confirmation_modal.cancel": "إلغاء", - "confirmations.block.confirm": "حجب", - "confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟", - "confirmations.delete.confirm": "حذف", - "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟", - "confirmations.mute.confirm": "أكتم", - "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", - "emoji_button.activity": "الأنشطة", - "emoji_button.flags": "الأعلام", - "emoji_button.food": "الطعام والشراب", - "emoji_button.label": "أدرج إيموجي", - "emoji_button.nature": "الطبيعة", - "emoji_button.objects": "أشياء", - "emoji_button.people": "الناس", - "emoji_button.search": "ابحث...", - "emoji_button.symbols": "رموز", - "emoji_button.travel": "أماكن و أسفار", - "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.", - "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.", - "empty_column.home.public_timeline": "الخيط العام", - "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.", - "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", - "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.", - "follow_request.authorize": "ترخيص", - "follow_request.reject": "رفض", - "getting_started.apps": "عدة تطبيقات مختلفة متوفرة", - "getting_started.heading": "إستعدّ للبدء", - "getting_started.about_addressing": "يمكنك متابعة الأشخاص إذا كنت تعرف اسم المستخدم الخاص بهم والنطاق الذي هم عليه عن طريق إدخال عنوان شبيه بالبريد الإلكتروني في الحقل المخصص للبحث.", - "getting_started.about_shortcuts": "إذا كان المستخدم المستهدف في نفس النطاق الذي تستخدمه، فإسم المستخدم وحده يكفي. وتنطبق نفس القاعدة على ذكر الأشخاص في المنشورات و التبويقات.", - "getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على GitHub {github}. {apps}.", - "home.column_settings.advanced": "متقدمة", - "home.column_settings.basic": "أساسية", - "home.column_settings.filter_regex": "تصفية حسب التعبيرات العادية", - "home.column_settings.show_reblogs": "عرض الترقيات", - "home.column_settings.show_replies": "عرض الردود", - "home.settings": "إعدادات العمود", - "lightbox.close": "إغلاق", - "loading_indicator.label": "تحميل ...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "تعذر العثور عليه", - "navigation_bar.blocks": "الحسابات المحجوبة", - "navigation_bar.community_timeline": "الخيط العام المحلي", - "navigation_bar.edit_profile": "تعديل الملف الشخصي", - "navigation_bar.preferences": "التفضيلات", - "navigation_bar.community_timeline": "الخيط العام المحلي", - "navigation_bar.public_timeline": "الخيط العام الموحد", - "navigation_bar.logout": "خروج", - "reply_indicator.cancel": "إلغاء", - "search.placeholder": "ابحث", - "search.account": "حساب", - "search.hashtag": "وسم", - "status.mention": "أذكُر @{name}", - "status.delete": "إحذف", - "status.reply": "ردّ", - "status.reblog": "رَقِّي", - "status.favourite": "أضف إلى المفضلة", - "status.reblogged_by": "{name} رقى", - "status.sensitive_warning": "محتوى حساس", - "status.sensitive_toggle": "اضغط للعرض", - "status.show_more": "أظهر المزيد", - "status.show_less": "إعرض أقلّ", - "status.open": "وسع هذه المشاركة", - "status.report": "إبلِغ عن @{name}", - "tabs_bar.compose": "تحرير", - "tabs_bar.home": "الرئيسية", - "tabs_bar.mentions": "الإشارات", - "tabs_bar.public": "الخيط العام الموحد", - "tabs_bar.notifications": "الإخطارات", - "upload_button.label": "إضافة وسائط", - "upload_form.undo": "إلغاء", - "upload_progress.label": "يرفع...", - "notification.follow": "{name} يتبعك", - "notification.favourite": "{name} أعجب بمنشورك", - "notification.reblog": "{name} قام بترقية تبويقك", - "notification.mention": "{name} ذكرك", - "notifications.column_settings.alert": "إشعارات سطح المكتب", - "notifications.column_settings.show": "إعرِضها في عمود", - "notifications.column_settings.follow": "متابعُون جُدُد :", - "notifications.column_settings.favourite": "المُفَضَّلة :", - "notifications.column_settings.mention": "الإشارات :", - "notifications.column_settings.reblog": "الترقيّات:", - "video_player.toggle_sound": "تبديل الصوت", - "video_player.toggle_visible": "إظهار / إخفاء الفيديو", - "video_player.expand": "وسّع الفيديو", - "video_player.video_error": "تعذر تشغيل الفيديو", -}; - -export default ar; diff --git a/app/assets/javascripts/components/locales/bg.jsx b/app/assets/javascripts/components/locales/bg.jsx @@ -1,68 +0,0 @@ -const bg = { - "column_back_button.label": "Назад", - "lightbox.close": "Затвори", - "loading_indicator.label": "Зареждане...", - "status.mention": "Споменаване", - "status.delete": "Изтриване", - "status.reply": "Отговор", - "status.reblog": "Споделяне", - "status.favourite": "Предпочитани", - "status.reblogged_by": "{name} сподели", - "status.sensitive_warning": "Деликатно съдържание", - "status.sensitive_toggle": "Покажи", - "video_player.toggle_sound": "Звук", - "account.mention": "Споменаване", - "account.edit_profile": "Редактирай профила си", - "account.unblock": "Не блокирай", - "account.unfollow": "Не следвай", - "account.block": "Блокирай", - "account.follow": "Последвай", - "account.posts": "Публикации", - "account.follows": "Следвам", - "account.followers": "Последователи", - "account.follows_you": "Твой последовател", - "account.requested": "В очакване на одобрение", - "getting_started.heading": "Първи стъпки", - "getting_started.about_addressing": "Можеш да последваш потребител, ако знаеш потребителското му име и домейна, на който се намира, като в полето за търсене ги въведеш по този начин: име@домейн", - "getting_started.about_shortcuts": "Ако с търсения потребител се намирате на един и същ домейн, достатъчно е да въведеш само името. Същото важи и за споменаване на хора в публикации.", - "getting_started.about_developer": "Можеш да потърсиш разработчика на този проект като: Gargron@mastodon.social", - "getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.", - "column.home": "Начало", - "column.mentions": "Споменавания", - "column.public": "Публичен канал", - "column.notifications": "Известия", - "tabs_bar.compose": "Съставяне", - "tabs_bar.home": "Начало", - "tabs_bar.mentions": "Споменавания", - "tabs_bar.public": "Публичен канал", - "tabs_bar.notifications": "Известия", - "compose_form.placeholder": "Какво си мислиш?", - "compose_form.publish": "Раздумай", - "compose_form.sensitive": "Отбележи съдържанието като деликатно", - "compose_form.spoiler": "Скрий текста зад предупреждение", - "compose_form.private": "Отбележи като поверително", - "compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?", - "compose_form.unlisted": "Не показвай в публичния канал", - "navigation_bar.edit_profile": "Редактирай профил", - "navigation_bar.preferences": "Предпочитания", - "navigation_bar.public_timeline": "Публичен канал", - "navigation_bar.logout": "Излизане", - "reply_indicator.cancel": "Отказ", - "search.placeholder": "Търсене", - "search.account": "Акаунт", - "search.hashtag": "Хаштаг", - "upload_button.label": "Добави медия", - "upload_form.undo": "Отмяна", - "notification.follow": "{name} те последва", - "notification.favourite": "{name} хареса твоята публикация", - "notification.reblog": "{name} сподели твоята публикация", - "notification.mention": "{name} те спомена", - "notifications.column_settings.alert": "Десктоп известия", - "notifications.column_settings.show": "Покажи в колона", - "notifications.column_settings.follow": "Нови последователи:", - "notifications.column_settings.favourite": "Предпочитани:", - "notifications.column_settings.mention": "Споменавания:", - "notifications.column_settings.reblog": "Споделяния:", -}; - -export default bg; diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx @@ -1,126 +0,0 @@ -const de = { - "account.block": "@{name} blocken", - "account.disclaimer": "Dieser Benutzer ist von einer anderen Instanz. Diese Zahl könnte größer sein.", - "account.edit_profile": "Profil bearbeiten", - "account.follow": "Folgen", - "account.followers": "Folgende", - "account.follows": "Folgt", - "account.follows_you": "Folgt dir", - "account.mention": "@{name} erwähnen", - "account.mute": "@{name} stummschalten", - "account.posts": "Beiträge", - "account.report": "@{name} melden", - "account.requested": "Warte auf Erlaubnis", - "account.unblock": "@{name} entblocken", - "account.unfollow": "Entfolgen", - "account.unmute": "@{name} nicht mehr stummschalten", - "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen", - "column_back_button.label": "Zurück", - "column.blocks": "Blockierte Benutzer", - "column.community": "Lokale Zeitleiste", - "column.favourites": "Favoriten", - "column.follow_requests": "Folgeanfragen", - "column.home": "Startseite", - "column.mutes": "Stummgeschaltete Benutzer", - "column.notifications": "Mitteilungen", - "column.public": "Gesamtes bekanntes Netz", - "compose_form.placeholder": "Worüber möchtest du schreiben?", - "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.", - "compose_form.publish": "Tröt", - "compose_form.sensitive": "Medien als heikel markieren", - "compose_form.spoiler_placeholder": "Inhaltswarnung", - "compose_form.spoiler": "Text hinter Warnung verbergen", - "emoji_button.label": "Emoji einfügen", - "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", - "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.", - "empty_column.home.public_timeline": "die öffentliche Zeitleiste", - "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.", - "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.", - "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.", - "follow_request.authorize": "Erlauben", - "follow_request.reject": "Ablehnen", - "getting_started.apps": "Es sind verschiedene Apps verfügbar", - "getting_started.heading": "Erste Schritte", - "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", - "home.column_settings.advanced": "Fortgeschritten", - "home.column_settings.basic": "Einfach", - "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke", - "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", - "home.column_settings.show_replies": "Antworten anzeigen", - "home.settings": "Spalteneinstellungen", - "lightbox.close": "Schließen", - "loading_indicator.label": "Lade…", - "media_gallery.toggle_visible": "Sichtbarkeit einstellen", - "missing_indicator.label": "Nicht gefunden", - "navigation_bar.blocks": "Blockierte Benutzer", - "navigation_bar.community_timeline": "Lokale Zeitleiste", - "navigation_bar.edit_profile": "Profil bearbeiten", - "navigation_bar.favourites": "Favoriten", - "navigation_bar.follow_requests": "Folgeanfragen", - "navigation_bar.info": "Erweiterte Informationen", - "navigation_bar.logout": "Abmelden", - "navigation_bar.mutes": "Stummgeschaltete Benutzer", - "navigation_bar.preferences": "Einstellungen", - "navigation_bar.public_timeline": "Föderierte Zeitleiste", - "notification.favourite": "{name} favorisierte deinen Status", - "notification.follow": "{name} folgt dir", - "notification.mention": "{name} erwähnte dich", - "notification.reblog": "{name} teilte deinen Status", - "notifications.clear_confirmation": "Bist du sicher, dass du alle Mitteilungen beseitigen willst?", - "notifications.clear": "Mitteilungen beseitigen", - "notifications.column_settings.alert": "Desktop-Benachrichtigungen", - "notifications.column_settings.favourite": "Favorisierungen:", - "notifications.column_settings.follow": "Neue Folgende:", - "notifications.column_settings.mention": "Erwähnungen:", - "notifications.column_settings.reblog": "Geteilte Beiträge:", - "notifications.column_settings.show": "In der Spalte anzeigen", - "notifications.column_settings.sound": "Ton abspielen", - "notifications.settings": "Spalteneinstellungen", - "privacy.change": "Privatsphäre des Status anpassen", - "privacy.direct.long": "Beitrag nur an erwähnte Benutzer", - "privacy.direct.short": "Direkt", - "privacy.private.long": "Beitrag nur an Folgende", - "privacy.private.short": "Privat", - "privacy.public.long": "Beitrag an öffentliche Zeitleisten", - "privacy.public.short": "Öffentlich", - "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen", - "privacy.unlisted.short": "Nicht gelistet", - "reply_indicator.cancel": "Abbrechen", - "report.heading": "Neue Meldung", - "report.placeholder": "Zusätzliche Kommentare", - "report.submit": "Absenden", - "report.target": "Melden", - "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", - "search.placeholder": "Suche", - "search.status_by": "Status von {name}", - "status.delete": "Löschen", - "status.favourite": "Favorisieren", - "status.load_more": "Weitere laden", - "status.media_hidden": "Medien versteckt", - "status.mention": "Erwähnen", - "status.open": "Öffnen", - "status.reblog": "Teilen", - "status.reblogged_by": "{name} teilte", - "status.reply": "Antworten", - "status.replyAll": "Auf Thread antworten", - "status.report": "@{name} melden", - "status.sensitive_toggle": "Klicke, um sie zu sehen", - "status.sensitive_warning": "Heikle Inhalte", - "status.show_less": "Weniger anzeigen", - "status.show_more": "Mehr anzeigen", - "tabs_bar.compose": "Schreiben", - "tabs_bar.federated_timeline": "Föderation", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Lokal", - "tabs_bar.notifications": "Mitteilungen", - "upload_area.title": "Hereinziehen zum Hochladen", - "upload_button.label": "Mediendatei hinzufügen", - "upload_form.undo": "Entfernen", - "upload_progress.label": "Lade hoch…", - "video_player.toggle_sound": "Ton umschalten", - "video_player.toggle_visible": "Sichtbarkeit umschalten", - "video_player.expand": "Videoanzeige vergrößern", - "video_player.video_error": "Video konnte nicht abgespielt werden", -}; - -export default de; diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx @@ -1,177 +0,0 @@ -/** - * Note for Contributors: - * This file (en.jsx) serve as a template for other languages. - * To make other contributors' life easier, please REMEMBER: - * 1. to add your new string here; and - * 2. to remove old strings that are no longer needed; and - * 3. to sort the strings by the key. - * 4. To rename the `en` const name and export default name to match your locale. - * Thanks! - */ -const en = { - "account.block": "Block @{name}", - "account.disclaimer": "This user is from another instance. This number may be larger.", - "account.edit_profile": "Edit profile", - "account.follow": "Follow", - "account.followers": "Followers", - "account.follows": "Follows", - "account.follows_you": "Follows you", - "account.mention": "Mention @{name}", - "account.mute": "Mute @{name}", - "account.posts": "Posts", - "account.report": "Report @{name}", - "account.requested": "Awaiting approval", - "account.unblock": "Unblock @{name}", - "account.unfollow": "Unfollow", - "account.unmute": "Unmute @{name}", - "boost_modal.combo": "You can press {combo} to skip this next time", - "column.blocks": "Blocked users", - "column.community": "Local timeline", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", - "column.home": "Home", - "column.mutes": "Muted users", - "column.notifications": "Notifications", - "column.public": "Federated timeline", - "column_back_button.label": "Back", - "column_subheading.navigation": "Navigation", - "column_subheading.settings": "Settings", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", - "compose_form.placeholder": "What is on your mind?", - "compose_form.privacy_disclaimer": "Your post will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is not a public post, and it may be boosted or otherwise made visible to unintended recipients.", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Mark media as sensitive", - "compose_form.spoiler": "Hide text behind warning", - "compose_form.spoiler_placeholder": "Content warning", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "emoji_button.activity": "Activity", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.search": "Search...", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", - "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", - "getting_started.apps": "Various apps are available", - "getting_started.heading": "Getting started", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", - "home.column_settings.advanced": "Advanced", - "home.column_settings.basic": "Basic", - "home.column_settings.filter_regex": "Filter out by regular expressions", - "home.column_settings.show_reblogs": "Show boosts", - "home.column_settings.show_replies": "Show replies", - "home.settings": "Column settings", - "lightbox.close": "Close", - "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.edit_profile": "Edit profile", - "navigation_bar.favourites": "Favourites", - "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.info": "Extended information", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Muted users", - "navigation_bar.preferences": "Preferences", - "navigation_bar.public_timeline": "Federated timeline", - "notification.favourite": "{name} favourited your status", - "notification.follow": "{name} followed you", - "notification.mention": "{name} mentioned you", - "notification.reblog": "{name} boosted your status", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "notifications.settings": "Column settings", - "onboarding.done": "Done", - "onboarding.next": "Next", - "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "onboarding.page_four.home": "The home timeline shows posts from people you follow.", - "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", - "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", - "onboarding.page_one.welcome": "Welcome to Mastodon!", - "onboarding.page_six.admin": "Your instance's admin is {admin}.", - "onboarding.page_six.almost_done": "Almost done...", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", - "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "onboarding.page_six.guidelines": "community guidelines", - "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", - "onboarding.page_six.various_app": "mobile apps", - "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", - "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", - "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "onboarding.skip": "Skip", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not post to public timelines", - "privacy.unlisted.short": "Unlisted", - "reply_indicator.cancel": "Cancel", - "report.heading": "New report", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Reporting", - "search.placeholder": "Search", - "search.status_by": "Status by {name}", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.cannot_reblog": "This post cannot be boosted", - "status.delete": "Delete", - "status.favourite": "Favourite", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", - "status.mention": "Mention @{name}", - "status.open": "Expand this status", - "status.reblog": "Boost", - "status.reblogged_by": "{name} boosted", - "status.reply": "Reply", - "status.replyAll": "Reply to thread", - "status.report": "Report @{name}", - "status.sensitive_toggle": "Click to view", - "status.sensitive_warning": "Sensitive content", - "status.show_less": "Show less", - "status.show_more": "Show more", - "tabs_bar.compose": "Compose", - "tabs_bar.federated_timeline": "Federated", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notifications", - "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media", - "upload_form.undo": "Undo", - "upload_progress.label": "Uploading...", - "video_player.expand": "Expand video", - "video_player.toggle_sound": "Toggle sound", - "video_player.toggle_visible": "Toggle visibility", - "video_player.video_error": "Video could not be played", -}; - -export default en; diff --git a/app/assets/javascripts/components/locales/eo.jsx b/app/assets/javascripts/components/locales/eo.jsx @@ -1,68 +0,0 @@ -const eo = { - "column_back_button.label": "Reveni", - "lightbox.close": "Fermi", - "loading_indicator.label": "Ŝarĝanta...", - "status.mention": "Mencii @{name}", - "status.delete": "Forigi", - "status.reply": "Respondi", - "status.reblog": "Diskonigi", - "status.favourite": "Favori", - "status.reblogged_by": "{name} diskonigita", - "status.sensitive_warning": "Tikla enhavo", - "status.sensitive_toggle": "Alklaki por vidi", - "video_player.toggle_sound": "Aktivigi sonojn", - "account.mention": "Mencii @{name}", - "account.edit_profile": "Redakti la profilon", - "account.unblock": "Malbloki @{name}", - "account.unfollow": "Malsekvi", - "account.block": "Bloki @{name}", - "account.follow": "Sekvi", - "account.posts": "Mesaĝoj", - "account.follows": "Sekvatoj", - "account.followers": "Sekvantoj", - "account.follows_you": "Sekvas vin", - "account.requested": "Atendas aprobon", - "getting_started.heading": "Por komenci", - "getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.", - "getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.", - "getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.", - "column.home": "Hejmo", - "column.community": "Loka tempolinio", - "column.public": "Fratara tempolinio", - "column.notifications": "Sciigoj", - "tabs_bar.compose": "Ekskribi", - "tabs_bar.home": "Hejmo", - "tabs_bar.mentions": "Sciigoj", - "tabs_bar.public": "Fratara tempolinio", - "tabs_bar.notifications": "Sciigoj", - "compose_form.placeholder": "Pri kio vi pensas?", - "compose_form.publish": "Hup", - "compose_form.sensitive": "Marki ke la enhavo estas tikla", - "compose_form.spoiler": "Kaŝi la tekston malantaŭ averto", - "compose_form.private": "Marki ke la enhavo estas privata", - "compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.", - "compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj", - "navigation_bar.edit_profile": "Redakti la profilon", - "navigation_bar.preferences": "Preferoj", - "navigation_bar.community_timeline": "Loka tempolinio", - "navigation_bar.public_timeline": "Fratara tempolinio", - "navigation_bar.logout": "Elsaluti", - "reply_indicator.cancel": "Rezigni", - "search.placeholder": "Serĉi", - "search.account": "Konto", - "search.hashtag": "Kradvorto", - "upload_button.label": "Aldoni enhavaĵon", - "upload_form.undo": "Malfari", - "notification.follow": "{name} sekvis vin", - "notification.favourite": "{name} favoris vian mesaĝon", - "notification.reblog": "{name} diskonigis vian mesaĝon", - "notification.mention": "{name} menciis vin", - "notifications.column_settings.alert": "Retumilaj atentigoj", - "notifications.column_settings.show": "Montri en kolono", - "notifications.column_settings.follow": "Novaj sekvantoj:", - "notifications.column_settings.favourite": "Favoroj:", - "notifications.column_settings.mention": "Mencioj:", - "notifications.column_settings.reblog": "Diskonigoj:", -}; - -export default eo; diff --git a/app/assets/javascripts/components/locales/es.jsx b/app/assets/javascripts/components/locales/es.jsx @@ -1,93 +0,0 @@ -const es = { - "column_back_button.label": "Atrás", - "lightbox.close": "Cerrar", - "loading_indicator.label": "Cargando...", - "status.mention": "Mencionar", - "status.delete": "Borrar", - "status.reply": "Responder", - "status.reblog": "Retoot", - "status.favourite": "Favorito", - "status.reblogged_by": "Retooteado por {name}", - "status.sensitive_warning": "Contenido sensible", - "status.sensitive_toggle": "Click para ver", - "status.show_more": "Mostrar más", - "status.show_less": "Mostrar menos", - "status.open": "Expandir estado", - "status.report": "Reportar", - "video_player.toggle_sound": "Act/Desac. sonido", - "account.mention": "Mencionar", - "account.edit_profile": "Editar perfil", - "account.unblock": "Desbloquear", - "account.unfollow": "Dejar de seguir", - "account.mute": "Silenciar", - "account.block": "Bloquear", - "account.follow": "Seguir", - "account.posts": "Publicaciones", - "account.follows": "Seguir", - "account.followers": "Seguidores", - "account.follows_you": "Te sigue", - "account.requested": "Esperando aprobación", - "getting_started.heading": "Primeros pasos", - "getting_started.about_addressing": "Puedes seguir a gente si conoces su nombre de usuario y el dominio en el que están registrados, introduciendo algo similar a una dirección de correo electrónico en el formulario en la parte superior de la barra lateral.", - "getting_started.about_shortcuts": "Si el usuario que buscas está en el mismo dominio que tú, simplemente funcionará introduciendo el nombre de usuario. La misma regla se aplica para mencionar a usuarios.", - "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}. {apps}.", - "column.home": "Inicio", - "column.community": "Historia local", - "column.public": "Historia federada", - "column.notifications": "Notificaciones", - "column.blocks": "Usuarios bloqueados", - "column.favourites": "Favoritos", - "column.follow_requests": "Solicitudes para seguirte", - "column.mutes": "Usuarios silenciados", - "tabs_bar.compose": "Redactar", - "tabs_bar.home": "Inicio", - "tabs_bar.mentions": "Menciones", - "tabs_bar.public": "Público", - "tabs_bar.notifications": "Notificaciones", - "compose_form.placeholder": "¿En qué estás pensando?", - "compose_form.publish": "Tootear", - "compose_form.sensitive": "Marcar contenido como sensible", - "compose_form.spoiler": "Ocultar texto tras advertencia", - "compose_form.spoiler_placeholder": "Advertencia de contenido", - "composer_form.private": "Marcar como privado", - "composer_form.privacy_disclaimer": "Tu estado se mostrará a los usuarios mencionados en {domains}. Tu estado podrá ser visto en otras instancias, quizás no quieras que tu estado sea visto por otros usuarios.", - "compose_form.unlisted": "No mostrar en la historia federada", - "navigation_bar.edit_profile": "Editar perfil", - "navigation_bar.preferences": "Preferencias", - "navigation_bar.community_timeline": "Historia local", - "navigation_bar.public_timeline": "Historia federada", - "navigation_bar.favourites": "Favoritos", - "navigation_bar.blocks": "Usuarios bloqueados", - "navigation_bar.info": "Información adicional", - "navigation_bar.logout": "Cerrar sesión", - "navigation_bar.follow_requests": "Solicitudes para seguirte", - "navigation_bar.mutes": "Usuarios silenciados", - "reply_indicator.cancel": "Cancelar", - "search.placeholder": "Buscar", - "search.account": "Cuenta", - "search.hashtag": "Etiqueta", - "upload_button.label": "Subir multimedia", - "upload_form.undo": "Deshacer", - "notification.follow": "{name} te empezó a seguir", - "notification.favourite": "{name} marcó tu estado como favorito", - "notification.reblog": "{name} ha retooteado tu estado", - "notification.mention": "{name} te ha mencionado", - "notifications.column_settings.alert": "Notificaciones de escritorio", - "notifications.column_settings.show": "Mostrar en columna", - "notifications.column_settings.follow": "Nuevos seguidores:", - "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.mention": "Menciones:", - "notifications.column_settings.reblog": "Retoots:", - "emoji_button.label": "Insertar emoji", - "privacy.public.short": "Público", - "privacy.public.long": "Mostrar en la historia federada", - "privacy.unlisted.short": "Sin federar", - "privacy.unlisted.long": "No mostrar en la historia federada", - "privacy.private.short": "Privado", - "privacy.private.long": "Sólo mostrar a seguidores", - "privacy.direct.short": "Directo", - "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", - "privacy.change": "Ajustar privacidad" -}; - -export default es; diff --git a/app/assets/javascripts/components/locales/fa.jsx b/app/assets/javascripts/components/locales/fa.jsx @@ -1,136 +0,0 @@ -const fa = { - "account.block": "@{name} را مسدود کن", - "account.disclaimer": "این کاربر عضو سرور متفاوتی است. شاید عدد واقعی بیشتر از این باشد.", - "account.edit_profile": "ویرایش نمایه", - "account.follow": "پی بگیرید", - "account.followers": "پیگیران", - "account.follows_you": "پیگیر شماست", - "account.follows": "پی می‌گیرد", - "account.mention": "نام‌بردن از @{name}", - "account.mute": "بی‌صدا کردن @{name}", - "account.posts": "نوشته‌ها", - "account.report": "گزارش @{name}", - "account.requested": "در انتظار پذیرش", - "account.unblock": "رفع انسداد @{name}", - "account.unfollow": "پایان پیگیری", - "account.unmute": "باصدا کردن @{name}", - "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", - "column_back_button.label": "بازگشت", - "column.blocks": "کاربران مسدودشده", - "column.community": "نوشته‌های محلی", - "column.favourites": "پسندیده‌ها", - "column.follow_requests": "درخواست‌های پیگیری", - "column.home": "خانه", - "column.mutes": "کاربران بی‌صداشده", - "column.notifications": "اعلان‌ها", - "column.public": "نوشته‌های همه‌جا", - "compose_form.placeholder": "تازه چه خبر؟", - "compose_form.privacy_disclaimer": "نوشتهٔ خصوصی شما به کاربران نام‌برده‌شده در {domains} فرستاده می‌شود. آیا به {domainsCount, plural, one {آن سرور} other {آن سرورها}} اعتماد دارید؟ تنظیمات حریم خصوصی نوشته‌ها تنها در سرورهای ماستدون کار می‌کند. اگر {domains} {domainsCount, plural, one {یک سرور ماستدون نباشد} other {سرورهای ماستدون نباشند}}، اشاره‌ای به خصوصی‌بودن نوشتهٔ شما نخواهد شد و شاید نوشتهٔ شما هم‌رسان شود یا برای کاربرانی که نمی‌خواهید نمایش یابد.", - "compose_form.publish": "بوق", - "compose_form.sensitive": "تصاویر حساس هستند", - "compose_form.spoiler_placeholder": "هشدار محتوا", - "compose_form.spoiler": "نوشته را پشت هشدار پنهان کنید", - "emoji_button.label": "افزودن شکلک", - "emoji_button.search": "جستجو...", - "emoji_button.people": "مردم", - "emoji_button.nature": "طبیعت", - "emoji_button.food": "غذا و نوشیدنی", - "emoji_button.activity": "فعالیت", - "emoji_button.travel": "سفر و مکان", - "emoji_button.objects": "اشیا", - "emoji_button.symbols": "نمادها", - "emoji_button.flags": "پرچم‌ها", - "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!", - "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.", - "empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا", - "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.", - "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.", - "empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود", - "follow_request.authorize": "اجازه دهید", - "follow_request.reject": "اجازه ندهید", - "getting_started.apps": "اپ‌های گوناگونی در دسترس‌اند", - "getting_started.heading": "آغاز کنید", - "getting_started.open_source_notice": "ماستدون یک نرم‌افزار آزاد است. می‌توانید در ساخت آن مشارکت کنید یا مشکلاتش را در {github} گزارش دهید. {apps}.", - "home.column_settings.advanced": "پیشرفته", - "home.column_settings.basic": "اصلی", - "home.column_settings.filter_regex": "با عبارت‌های باقاعده فیلتر کنید", - "home.column_settings.show_reblogs": "نمایش بازبوق‌ها", - "home.column_settings.show_replies": "نمایش پاسخ‌ها", - "home.settings": "تنظیمات ستون", - "lightbox.close": "بستن", - "loading_indicator.label": "بارگیری...", - "media_gallery.toggle_visible": "تغییر پیدایی", - "missing_indicator.label": "پیدا نشد", - "navigation_bar.blocks": "کاربران مسدودشده", - "navigation_bar.community_timeline": "نوشته‌های محلی", - "navigation_bar.edit_profile": "ویرایش نمایه", - "navigation_bar.favourites": "پسندیده‌ها", - "navigation_bar.follow_requests": "درخواست‌های پیگیری", - "navigation_bar.info": "اطلاعات تکمیلی", - "navigation_bar.logout": "خروج", - "navigation_bar.mutes": "کاربران بی‌صداشده", - "navigation_bar.preferences": "ترجیحات", - "navigation_bar.public_timeline": "نوشته‌های همه‌جا", - "notification.favourite": "{name} نوشتهٔ شما را پسندید", - "notification.follow": "{name} پیگیر شما شد", - "notification.mention": "{name} از شما نام برد", - "notification.reblog": "{name} نوشتهٔ شما را بازبوقید", - "notifications.clear_confirmation": "واقعاً می‌خواهید همهٔ اعلان‌هایتان را برای همیشه پاک کنید؟", - "notifications.clear": "پاک‌کردن اعلان‌ها", - "notifications.column_settings.alert": "اعلان در کامپیوتر", - "notifications.column_settings.favourite": "پسندیده‌ها:", - "notifications.column_settings.follow": "پیگیران تازه:", - "notifications.column_settings.mention": "نام‌بردن‌ها:", - "notifications.column_settings.reblog": "بازبوق‌ها:", - "notifications.column_settings.show": "در ستون نشان بده", - "notifications.column_settings.sound": "صدا را پخش کن", - "notifications.settings": "تنظیمات ستون", - "privacy.change": "تنظیم حریم خصوصی نوشته‌ها", - "privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده", - "privacy.direct.short": "مستقیم", - "privacy.private.long": "تنها به پیگیران نشان بده", - "privacy.private.short": "خصوصی", - "privacy.public.long": "در فهرست نوشته‌های عمومی نشان بده", - "privacy.public.short": "عمومی", - "privacy.unlisted.long": "در فهرست نوشته‌های همه‌جا نشان نده", - "privacy.unlisted.short": "فهرست‌نشده", - "reply_indicator.cancel": "لغو", - "report.heading": "گزارش تازه", - "report.placeholder": "توضیح اضافه", - "report.submit": "بفرست", - "report.target": "گزارش‌دادن", - "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", - "search.placeholder": "جستجو", - "search.status_by": "نوشتهٔ {name}", - "status.delete": "پاک‌کردن", - "status.favourite": "پسندیدن", - "status.load_more": "بیشتر نشان بده", - "status.media_hidden": "تصویر پنهان شده", - "status.mention": "از @{name} نام ببرید", - "status.open": "این نوشته را باز کن", - "status.reblog": "بوق", - "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", - "status.reblogged_by": "{name} بازبوقید", - "status.reply": "پاسخ", - "status.replyAll": "به نوشته پاسخ دهید", - "status.report": "@{name} را گزارش دهید", - "status.sensitive_toggle": "برای دیدن کلیک کنید", - "status.sensitive_warning": "محتوای حساس", - "status.show_less": "نهفتن", - "status.show_more": "نمایش", - "tabs_bar.compose": "بنویسید", - "tabs_bar.federated_timeline": "همگانی", - "tabs_bar.home": "خانه", - "tabs_bar.local_timeline": "محلی", - "tabs_bar.notifications": "اعلان‌ها", - "upload_area.title": "برای بارگذاری به این‌جا بکشید", - "upload_button.label": "افزودن تصویر", - "upload_form.undo": "واگردانی", - "upload_progress.label": "بارگذاری...", - "video_player.toggle_sound": "تغییر صداداری", - "video_player.toggle_visible": "تغییر پیدایی", - "video_player.expand": "بازکردن ویدیو", - "video_player.video_error": "ویدیو نمی‌تواند پخش شود", -}; - -export default fa; diff --git a/app/assets/javascripts/components/locales/fi.jsx b/app/assets/javascripts/components/locales/fi.jsx @@ -1,68 +0,0 @@ -const fi = { - "column_back_button.label": "Takaisin", - "lightbox.close": "Sulje", - "loading_indicator.label": "Ladataan...", - "status.mention": "Mainitse @{name}", - "status.delete": "Poista", - "status.reply": "Vastaa", - "status.reblog": "Buustaa", - "status.favourite": "Tykkää", - "status.reblogged_by": "{name} buustasi", - "status.sensitive_warning": "Arkaluontoista sisältöä", - "status.sensitive_toggle": "Klikkaa nähdäksesi", - "video_player.toggle_sound": "Äänet päälle/pois", - "account.mention": "Mainitse @{name}", - "account.edit_profile": "Muokkaa", - "account.unblock": "Salli @{name}", - "account.unfollow": "Lopeta seuraaminen", - "account.block": "Estä @{name}", - "account.follow": "Seuraa", - "account.posts": "Postit", - "account.follows": "Seuraa", - "account.followers": "Seuraajia", - "account.follows_you": "Seuraa sinua", - "account.requested": "Odottaa hyväksyntää", - "getting_started.heading": "Aloitus", - "getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.", - "getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi", - "getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}. {apps}.", - "column.home": "Koti", - "column.community": "Paikallinen aikajana", - "column.public": "Yleinen aikajana", - "column.notifications": "Ilmoitukset", - "tabs_bar.compose": "Luo", - "tabs_bar.home": "Koti", - "tabs_bar.mentions": "Maininnat", - "tabs_bar.public": "Yleinen aikajana", - "tabs_bar.notifications": "Ilmoitukset", - "compose_form.placeholder": "Mitä sinulla on mielessä?", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Merkitse media herkäksi", - "compose_form.spoiler": "Piiloita teksti varoituksen taakse", - "compose_form.private": "Merkitse yksityiseksi", - "compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.", - "compose_form.unlisted": "Älä näytä yleisillä aikajanoilla", - "navigation_bar.edit_profile": "Muokkaa profiilia", - "navigation_bar.preferences": "Ominaisuudet", - "navigation_bar.community_timeline": "Paikallinen aikajana", - "navigation_bar.public_timeline": "Yleinen aikajana", - "navigation_bar.logout": "Kirjaudu ulos", - "reply_indicator.cancel": "Peruuta", - "search.placeholder": "Hae", - "search.account": "Tili", - "search.hashtag": "Hashtag", - "upload_button.label": "Lisää mediaa", - "upload_form.undo": "Peru", - "notification.follow": "{name} seurasi sinua", - "notification.favourite": "{name} tykkäsi statuksestasi", - "notification.reblog": "{name} buustasi statustasi", - "notification.mention": "{name} mainitsi sinut", - "notifications.column_settings.alert": "Työpöytä ilmoitukset", - "notifications.column_settings.show": "Näytä sarakkeessa", - "notifications.column_settings.follow": "Uusia seuraajia:", - "notifications.column_settings.favourite": "Tykkäyksiä:", - "notifications.column_settings.mention": "Mainintoja:", - "notifications.column_settings.reblog": "Buusteja:", -}; - -export default fi; diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx @@ -1,155 +0,0 @@ -/** - * Note aux contributeurs⋅trices: - * Pour rendre plus simple la vie des autres personnes - * apportant leur contribution, merci de penser aux choses suivantes : - * 1. Ajoutez les nouvelles chaînes traduites par ordre alphabétique - * 2. Pensez à supprimer les chaînes inutilisées - * Merci ! - */ -const fr = { - "account.block": "Bloquer", - "account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.", - "account.edit_profile": "Modifier le profil", - "account.followers": "Abonné⋅e⋅s", - "account.follows": "Abonnements", - "account.follow": "Suivre", - "account.follows_you": "Vous suit", - "account.mention": "Mentionner", - "account.mute": "Masquer", - "account.posts": "Statuts", - "account.report": "Signaler", - "account.requested": "Invitation envoyée", - "account.unblock": "Débloquer", - "account.unfollow": "Ne plus suivre", - "account.unmute": "Ne plus masquer", - "column_back_button.label": "Retour", - "column.blocks": "Comptes bloqués", - "column.community": "Fil public local", - "column.favourites": "Favoris", - "column.follow_requests": "Demandes de suivi", - "column.home": "Accueil", - "column.notifications": "Notifications", - "column.public": "Fil public global", - "compose_form.placeholder": "Qu’avez-vous en tête ?", - "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", - "compose_form.private": "Rendre privé", - "compose_form.publish": "Pouet", - "compose_form.sensitive": "Marquer le média comme délicat", - "compose_form.spoiler": "Masquer le texte derrière un avertissement", - "compose_form.spoiler_placeholder": "Avertissement", - "compose_form.unlisted": "Ne pas afficher dans les fils publics", - "emoji_button.label": "Insérer un emoji", - "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", - "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag", - "empty_column.home.public_timeline": "le fil public", - "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.", - "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", - "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.", - "follow_request.authorize": "Autoriser", - "follow_request.reject": "Rejeter", - "getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champ de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", - "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", - "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", - "getting_started.heading": "Pour commencer", - "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", - "home.column_settings.advanced": "Avancé", - "home.column_settings.basic": "Basique", - "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", - "home.column_settings.show_reblogs": "Afficher les partages", - "home.column_settings.show_replies": "Afficher les réponses", - "home.settings": "Paramètres de la colonne", - "lightbox.close": "Fermer", - "loading_indicator.label": "Chargement…", - "media_gallery.toggle_visible": "Modifier la visibilité", - "missing_indicator.label": "Non trouvé", - "navigation_bar.blocks": "Comptes bloqués", - "navigation_bar.community_timeline": "Fil public local", - "navigation_bar.edit_profile": "Modifier le profil", - "navigation_bar.favourites": "Favoris", - "navigation_bar.follow_requests": "Demandes de suivi", - "navigation_bar.info": "Plus d’informations", - "navigation_bar.logout": "Déconnexion", - "navigation_bar.mutes": "Comptes silencés", - "navigation_bar.preferences": "Préférences", - "navigation_bar.public_timeline": "Fil public global", - "notification.favourite": "{name} a ajouté à ses favoris :", - "notification.follow": "{name} vous suit.", - "notification.mention": "{name} vous a mentionné⋅e :", - "notification.reblog": "{name} a partagé votre statut :", - "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", - "notifications.clear": "Nettoyer", - "notifications.column_settings.alert": "Notifications locales", - "notifications.column_settings.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux abonné⋅e⋅s :", - "notifications.column_settings.mention": "Mentions :", - "notifications.column_settings.reblog": "Partages :", - "notifications.column_settings.show": "Afficher dans la colonne", - "notifications.column_settings.sound": "Émettre un son", - "notifications.settings": "Paramètres de la colonne", - "onboarding.next": "Suivant", - "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.", - "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez", - "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous", - "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", - "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}", - "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", - "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}", - "onboarding.page_six.almost_done": "Nous y sommes presque…", - "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!", - "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", - "onboarding.page_six.guidelines": "règles de la communauté", - "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", - "onboarding.page_six.various_app": "applications mobiles", - "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.", - "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.", - "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", - "onboarding.skip": "Passer", - "privacy.change": "Ajuster la confidentialité du message", - "privacy.direct.long": "N’afficher que pour les personnes mentionnées", - "privacy.direct.short": "Direct", - "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", - "privacy.private.short": "Privé", - "privacy.public.long": "Afficher dans les fils publics", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Ne pas afficher dans les fils publics", - "privacy.unlisted.short": "Non-listé", - "reply_indicator.cancel": "Annuler", - "report.heading": "Nouveau signalement", - "report.placeholder": "Commentaires additionnels", - "report.submit": "Envoyer", - "report.target": "Signalement", - "search.account": "Compte", - "search.hashtag": "Mot-clé", - "search.placeholder": "Rechercher", - "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", - "search.status_by": "Statuts de {name}", - "status.delete": "Effacer", - "status.favourite": "Ajouter aux favoris", - "status.load_more": "Charger plus", - "status.media_hidden": "Média caché", - "status.mention": "Mentionner", - "status.open": "Déplier ce statut", - "status.reblogged_by": "{name} a partagé :", - "status.reblog": "Partager", - "status.reply": "Répondre", - "status.report": "Signaler @{name}", - "status.sensitive_toggle": "Cliquer pour dévoiler", - "status.sensitive_warning": "Contenu délicat", - "status.show_less": "Replier", - "status.show_more": "Déplier", - "tabs_bar.compose": "Composer", - "tabs_bar.federated_timeline": "Fil public global", - "tabs_bar.home": "Accueil", - "tabs_bar.local_timeline": "Fil public local", - "tabs_bar.mentions": "Mentions", - "tabs_bar.notifications": "Notifications", - "tabs_bar.public": "Fil public global", - "upload_area.title": "Glissez et déposez pour envoyer", - "upload_button.label": "Joindre un média", - "upload_form.undo": "Annuler", - "upload_progress.label": "Envoi en cours…", - "video_player.toggle_sound": "Mettre/Couper le son", - "video_player.toggle_visible": "Afficher/Cacher la vidéo", -}; - -export default fr; diff --git a/app/assets/javascripts/components/locales/he.jsx b/app/assets/javascripts/components/locales/he.jsx @@ -1,177 +0,0 @@ -/** - * הערה לתורמים: - * קובץ זה (he.jsx)מבוסס על en.jsx ויש לעדכנו מפעם לפעם כשיוצאות גרסאות חדשות. - * אנא הקלו על התורמים העתידיים: - * 1. הוסיפו לכאן מחרוזות חדשות - * 2. הסירו מחרוזות ישנות שכבר לא בשימוש בגרסא האנגלית - * 3. מיינו את השורות לפי סדר ABC כמו בקובץ המקורי. - * 4. ובבקשה כבדו את סגנון התרגום שהנחלנו כאן, או תאמו איתנו אם ישנם שינויים יסודיים - * תודה! - */ -const he = { - "account.block": "חסימת @{name}", - "account.disclaimer": "&rlm;משתמש זה מגיע מקהילה אחרת. המספר הזה עשוי להיות גדול יותר.", - "account.edit_profile": "עריכת פרופיל", - "account.follow": "מעקב", - "account.followers": "עוקבים", - "account.follows_you": "במעקב אחריך", - "account.follows": "נעקבים", - "account.mention": "אזכור של @{name}", - "account.mute": "להשתיק את @{name}", - "account.posts": "הודעות", - "account.report": "לדווח על @{name}", - "account.requested": "בהמתנה לאישור", - "account.unblock": "הסרת חסימה מעל @{name}", - "account.unfollow": "הפסקת מעקב", - "account.unmute": "הפסקת השתקת @{name}", - "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה", - "column.blocks": "חסימות", - "column.community": "פיד מקומי", - "column.favourites": "חיבובים", - "column.follow_requests": "בקשות מעקב", - "column.home": "בבית", - "column.mutes": "השתקות", - "column.notifications": "התראות", - "column.public": "בפרהסיה", - "column_back_button.label": "אחורה", - "column_subheading.navigation": "ניווט", - "column_subheading.settings": "אפשרויות", - "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.", - "compose_form.lock_disclaimer.lock": "נעול", - "compose_form.placeholder": "&rlm;מה עובר לך בראש?", - "compose_form.privacy_disclaimer": "&rlm;הודעתך הפרטית תשלח למשתמשים על {domains}. האם ניתן לסמוך על {domainsCount, plural, one {שרת זה} other {שרתים אלו}}? פרטיות ההודעה קיימת רק על שרתי מסטודון. אם {domains} {domainsCount, plural, one {הוא לא שרת מסטודון} other {הם לא שרתי מסטודון}}, לא יהיה שום סימן שההודעה פרטית, והוא עשוי להיות מקודם או להחשף למשתמשים שלא ברשימת היעד.", - "compose_form.publish": "&rlm;לחצרץ", - "compose_form.sensitive": "סימון תוכן כרגיש", - "compose_form.spoiler": "הסתרה מאחורי אזהרת תוכן", - "compose_form.spoiler_placeholder": "אזהרת תוכן", - "confirmation_modal.cancel": "ביטול", - "confirmations.block.confirm": "לחסום", - "confirmations.block.message": "לחסום את {name}?", - "confirmations.delete.confirm": "למחוק", - "confirmations.delete.message": "למחוק את ההודעה?", - "confirmations.mute.confirm": "להשתיק", - "confirmations.mute.message": "להשתיק את {name}?", - "emoji_button.activity": "פעילות", - "emoji_button.flags": "דגלים", - "emoji_button.food": "אוכל ושתיה", - "emoji_button.label": "הוספת אמוג'י", - "emoji_button.nature": "טבע", - "emoji_button.objects": "חפצים", - "emoji_button.people": "אנשים", - "emoji_button.search": "&rlm;חיפוש...", - "emoji_button.symbols": "סמלים", - "emoji_button.travel": "טיולים ואתרים", - "empty_column.community": "&rlm;טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!", - "empty_column.hashtag": "&rlm;אין כלום בהאשתג הזה עדיין.", - "empty_column.home.public_timeline": "בפרהסיה", - "empty_column.home": "&rlm;אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.", - "empty_column.notifications": "&rlm;אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!", - "empty_column.public": "&rlm;אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.", - "follow_request.authorize": "קבלה", - "follow_request.reject": "דחיה", - "getting_started.apps": "קיים מבחר יישומונים לניידים", - "getting_started.heading": "על ההתחלה", - "getting_started.open_source_notice": "מסטודון היא תוכנה חופשית (בקוד פתוח). ניתן לתרום או לדווח על בעיות בגיטהאב: {github}. {apps}.", - "home.column_settings.advanced": "למתקדמים", - "home.column_settings.basic": "למתחילים", - "home.column_settings.filter_regex": "&rlm;סינון באמצעות ביטויים רגולריים (regular expressions)", - "home.column_settings.show_reblogs": "הצגת הדהודים", - "home.column_settings.show_replies": "הצגת תגובות", - "home.settings": "הגדרות טור", - "lightbox.close": "סגירה", - "loading_indicator.label": "טוען...", - "media_gallery.toggle_visible": "נראה\\בלתי נראה", - "missing_indicator.label": "לא נמצא", - "navigation_bar.blocks": "חסימות", - "navigation_bar.community_timeline": "פיד מקומי", - "navigation_bar.edit_profile": "עריכת פרופיל", - "navigation_bar.favourites": "חיבובים", - "navigation_bar.follow_requests": "בקשות מעקב", - "navigation_bar.info": "מידע נוסף", - "navigation_bar.logout": "יציאה", - "navigation_bar.mutes": "השתקות", - "navigation_bar.preferences": "העדפות", - "navigation_bar.public_timeline": "בפרהסיה", - "notification.favourite": "חצרוצך חובב על ידי {name}", - "notification.follow": "{name} במעקב אחרייך", - "notification.mention": 'אוזכרת ע"י {name}', - "notification.reblog": "חצרוצך הודהד על ידי {name}", - "notifications.clear": "הסרת התראות", - "notifications.clear_confirmation": "&rlm;להסיר את כל ההתראות? בטוח?", - "notifications.column_settings.alert": "התראות לשולחן העבודה", - "notifications.column_settings.favourite": "מחובבים:", - "notifications.column_settings.follow": "עוקבים חדשים:", - "notifications.column_settings.mention": "&rlm;פניות:", - "notifications.column_settings.reblog": "&rlm;הדהודים:", - "notifications.column_settings.show": "הצגה בטור", - "notifications.column_settings.sound": "שמע מופעל", - "notifications.settings": "הגדרות טור", - "onboarding.done": "יציאה", - "onboarding.next": "הלאה", - "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.", - "onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.", - "onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.", - "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו: קהילות", - "onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}", - "onboarding.page_one.welcome": "ברוכים הבאים למסטודון!", - "onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.", - "onboarding.page_six.almost_done": "כמעט סיימנו...", - "onboarding.page_six.appetoot": "בתותאבון!", - "onboarding.page_six.apps_available": "קיימים {apps} זמינים עבור אנדרואיד, אייפון ופלטפורמות נוספות.", - "onboarding.page_six.github": "מסטודון הוא תוכנה חופשית. ניתן לדווח על באגים, לבקש יכולות, או לתרום לקוד באתר {github}.", - "onboarding.page_six.guidelines": "חוקי הקהילה", - "onboarding.page_six.read_guidelines": "&rlm;נא לקרוא את {guidelines} של {domain}!", - "onboarding.page_six.various_app": "יישומונים ניידים", - "onboarding.page_three.profile": "ץתחת 'עריכת פרופיל' ניתן להחליף את תמונת הפרופיל שלך, תיאור קצר, והשם המוצג. שם גם ניתן למצוא אפשרויות והעדפות נוספות.", - "onboarding.page_three.search": "בחלונית החיפוש ניתן לחפש אנשים והאשתגים, כמו למשל {illustration} או {introductions}. כדי למצוא מישהו שלא על האינסטנס המקומי, יש להשתמש בכינוי המשתמש המלא.", - "onboarding.page_two.compose": "הודעות כותבים מטור הכתיבה. ניתן לנעלות תמונות, לשנות הגדרות פרטיות, ולהוסיף אזהרות תוכן בעזרת האייקונים שמתחת.", - "onboarding.skip": "לדלג", - "privacy.change": "שינוי פרטיות ההודעה", - "privacy.direct.long": "הצג רק למי שהודעה זו פונה אליו", - "privacy.direct.short": "הודעה ישירה", - "privacy.private.long": "הצג לעוקבים מקומיים בלבד", - "privacy.private.short": "לעוקבים בלבד", - "privacy.public.long": "פרסם בפומבי", - "privacy.public.short": "פומבי", - "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", - "privacy.unlisted.short": "לא לפיד הכללי", - "reply_indicator.cancel": "ביטול", - "report.heading": "דווח חדש", - "report.placeholder": "הערות נוספות", - "report.submit": "שליחה", - "report.target": "דיווח", - "search.placeholder": "חיפוש", - "search.status_by": "הודעה מאת {name}", - "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", - "status.cannot_reblog": "לא ניתן להדהד הודעה זו", - "status.delete": "מחיקה", - "status.favourite": "חיבוב", - "status.load_more": "עוד", - "status.media_hidden": "מדיה מוסתרת", - "status.mention": "פניה אל @{name}", - "status.open": "הרחבת הודעה", - "status.reblog": "הדהוד", - "status.reblogged_by": "הודהד על ידי {name}", - "status.reply": "תגובה", - "status.replyAll": "תגובה לכולם", - "status.report": "דיווח על @{name}", - "status.sensitive_warning": "תוכן רגיש", - "status.sensitive_toggle": "לחצו כדי לראות", - "status.show_less": "הראה פחות", - "status.show_more": "הראה יותר", - "tabs_bar.compose": "חיבור", - "tabs_bar.federated_timeline": "בפדרציה", - "tabs_bar.home": "בבית", - "tabs_bar.local_timeline": "פיד מקומי", - "tabs_bar.notifications": "התראות", - "upload_area.title": "ניתן להעלות על ידי Drag & drop", - "upload_button.label": "הוספת מדיה", - "upload_form.undo": "ביטול", - "upload_progress.label": "עולה...", - "video_player.expand": "הרחבת וידאו", - "video_player.toggle_sound": "הפעלת\\ביטול שמע", - "video_player.toggle_visible": "הפעלת\\ביטול תצוגה", - "video_player.video_error": "לא ניתן לנגן וידאו", -}; - -export default he; diff --git a/app/assets/javascripts/components/locales/hr.jsx b/app/assets/javascripts/components/locales/hr.jsx @@ -1,121 +0,0 @@ -const hr = { - "account.block": "Blokiraj @{name}", - "account.disclaimer": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.", - "account.edit_profile": "Uredi profil", - "account.follow": "Slijedi", - "account.followers": "Sljedbenici", - "account.follows_you": "te slijedi", - "account.follows": "Slijedi", - "account.mention": "Spomeni @{name}", - "account.mute": "Utišaj @{name}", - "account.posts": "Postovi", - "account.report": "Prijavi @{name}", - "account.requested": "Čeka pristanak", - "account.unblock": "Deblokiraj @{name}", - "account.unfollow": "Prestani slijediti", - "account.unmute": "Poništi utišavanje @{name}", - "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", - "column_back_button.label": "Natrag", - "column.blocks": "Blokirani korisnici", - "column.community": "Lokalni timeline", - "column.favourites": "Favoriti", - "column.follow_requests": "Zahtjevi za slijeđenje", - "column.home": "Dom", - "column.notifications": "Notifikacije", - "column.public": "Federalni timeline", - "compose_form.placeholder": "Što ti je na umu?", - "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bit biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Označi media sadržaj kao osjetljiv", - "compose_form.spoiler_placeholder": "Upozorenje o sadržaju", - "compose_form.spoiler": "Sakrij text iza upozorenja", - "emoji_button.label": "Umetni smajlije", - "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", - "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", - "empty_column.home.public_timeline": "javni timeline", - "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", - "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", - "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", - "follow_request.authorize": "Authoriziraj", - "follow_request.reject": "Odbij", - "getting_started.apps": "Dostupne su razne aplikacije", - "getting_started.heading": "Počnimo", - "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}. {apps}.", - "home.column_settings.advanced": "Napredno", - "home.column_settings.basic": "Osnovno", - "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima", - "home.column_settings.show_reblogs": "Pokaži boosts", - "home.column_settings.show_replies": "Pokaži odgovore", - "home.settings": "Postavke Stupca", - "lightbox.close": "Zatvori", - "loading_indicator.label": "Učitavam...", - "media_gallery.toggle_visible": "Preklopi vidljivost", - "missing_indicator.label": "Nije nađen", - "navigation_bar.blocks": "Blokirani korisnici", - "navigation_bar.community_timeline": "Lokalni timeline", - "navigation_bar.edit_profile": "Uredi profil", - "navigation_bar.favourites": "Favoriti", - "navigation_bar.follow_requests": "Zahtjevi za sljeđenje", - "navigation_bar.info": "Proširena informacija", - "navigation_bar.logout": "Odjavi se", - "navigation_bar.preferences": "Postavke", - "navigation_bar.public_timeline": "Federalni timeline", - "notification.favourite": "{name} je lajkao tvoj status", - "notification.follow": "{name} te sada slijedi", - "notification.reblog": "{name} je podigao tvoj status", - "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?", - "notifications.clear": "Očisti notifikacije", - "notifications.column_settings.alert": "Desktop notifikacije", - "notifications.column_settings.favourite": "Favoriti:", - "notifications.column_settings.follow": "Novi sljedbenici:", - "notifications.column_settings.mention": "Spominjanja:", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Prikaži u stupcu", - "notifications.column_settings.sound": "Sviraj zvuk", - "notifications.settings": "Postavke rubrike", - "privacy.change": "Podesi status privatnosti", - "privacy.direct.long": "Prikaži samo spomenutim korisnicima", - "privacy.direct.short": "Direktno", - "privacy.private.long": "Prikaži samo sljedbenicima", - "privacy.private.short": "Privatno", - "privacy.public.long": "Postaj na javne timeline", - "privacy.public.short": "Javno", - "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", - "privacy.unlisted.short": "Unlisted", - "reply_indicator.cancel": "Otkaži", - "report.heading": "Nova prijava", - "report.placeholder": "Dodatni komentari", - "report.submit": "Pošalji", - "report.target": "Prijavljivanje", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "search.placeholder": "Traži", - "search.status_by": "Status od {name}", - "status.delete": "Obriši", - "status.favourite": "Označi omiljenim", - "status.load_more": "Učitaj više", - "status.media_hidden": "Sakriven media sadržaj", - "status.mention": "Spomeni @{name}", - "status.open": "Proširi ovaj status", - "status.reblog": "Podigni", - "status.reblogged_by": "{name} je podigao", - "status.reply": "Odgovori", - "status.report": "Prijavi @{name}", - "status.sensitive_toggle": "Klikni da bi vidio", - "status.sensitive_warning": "Osjetljiv sadržaj", - "status.show_less": "Pokaži manje", - "status.show_more": "Pokaži više", - "tabs_bar.compose": "Sastavi", - "tabs_bar.federated_timeline": "Federalni", - "tabs_bar.home": "Dom", - "tabs_bar.local_timeline": "Lokalno", - "tabs_bar.notifications": "Notifikacije", - "upload_area.title": "Povuci & spusti kako bi uploadao", - "upload_button.label": "Dodaj media", - "upload_form.undo": "Poništi", - "upload_progress.label": "Uploadam...", - "video_player.toggle_sound": "Toggle zvuk", - "video_player.toggle_visible": "Preklopi vidljivost", - "video_player.expand": "Proširi video", -}; - -export default hr; diff --git a/app/assets/javascripts/components/locales/hu.jsx b/app/assets/javascripts/components/locales/hu.jsx @@ -1,57 +0,0 @@ -const hu = { - "column_back_button.label": "Vissza", - "lightbox.close": "Bezárás", - "loading_indicator.label": "Betöltés...", - "status.mention": "Említés", - "status.delete": "Törlés", - "status.reply": "Válasz", - "status.reblog": "Reblog", - "status.favourite": "Kedvenc", - "status.reblogged_by": "{name} reblogolta", - "status.sensitive_warning": "Érzékeny tartalom", - "status.sensitive_toggle": "Katt a megtekintéshez", - "video_player.toggle_sound": "Hang kapcsolása", - "account.mention": "Említés", - "account.edit_profile": "Profil szerkesztése", - "account.unblock": "Blokkolás levétele", - "account.unfollow": "Követés abbahagyása", - "account.block": "Blokkolás", - "account.follow": "Követés", - "account.posts": "Posts", - "account.follows": "Követve", - "account.followers": "Követők", - "account.follows_you": "Követnek téged", - "getting_started.heading": "Első lépések", - "getting_started.about_addressing": "Követhetsz embereket felhasználónevük és a doménjük ismeretében, amennyiben megadod ezt az e-mail-szerű címet az oldalsáv tetején lévő rubrikában.", - "getting_started.about_shortcuts": "Ha a célzott személy azonos doménen tartózkodik, a felhasználónév elegendő. Ugyanez érvényes mikor személyeket említesz az állapotokban.", - "getting_started.about_developer": "A projekt fejlesztője követhető, mint Gargron@mastodon.social", - "column.home": "Kezdőlap", - "column.mentions": "Említések", - "column.public": "Nyilvános", - "column.notifications": "Értesítések", - "tabs_bar.compose": "Összeállítás", - "tabs_bar.home": "Kezdőlap", - "tabs_bar.mentions": "Említések", - "tabs_bar.public": "Nyilvános", - "tabs_bar.notifications": "Notifications", - "compose_form.placeholder": "Mire gondolsz?", - "compose_form.publish": "Tülk!", - "compose_form.sensitive": "Tartalom érzékenynek jelölése", - "compose_form.unlisted": "Listázatlan mód", - "navigation_bar.edit_profile": "Profil szerkesztése", - "navigation_bar.preferences": "Beállítások", - "navigation_bar.public_timeline": "Nyilvános időfolyam", - "navigation_bar.logout": "Kijelentkezés", - "reply_indicator.cancel": "Mégsem", - "search.placeholder": "Keresés", - "search.account": "Fiók", - "search.hashtag": "Hashtag", - "upload_button.label": "Média hozzáadása", - "upload_form.undo": "Mégsem", - "notification.follow": "{name} követ téged", - "notification.favourite": "{name} kedvencnek jelölte az állapotod", - "notification.reblog": "{name} reblogolta az állapotod", - "notification.mention": "{name} megemlített" -}; - -export default hu; diff --git a/app/assets/javascripts/components/locales/id.jsx b/app/assets/javascripts/components/locales/id.jsx @@ -1,167 +0,0 @@ -const id = { - "account.block": "Blokir @{name}", - "account.disclaimer": "Pengguna ini berasal dari server lain. Angka berikut mungkin lebih besar.", - "account.edit_profile": "Ubah profil", - "account.follow": "Ikuti", - "account.followers": "Pengikut", - "account.follows_you": "Mengikuti anda", - "account.follows": "Mengikuti", - "account.mention": "Balasan @{name}", - "account.mute": "Bisukan @{name}", - "account.posts": "Postingan", - "account.report": "Laporkan @{name}", - "account.requested": "Menunggu persetujuan", - "account.unblock": "Hapus blokir @{name}", - "account.unfollow": "Berhenti mengikuti", - "account.unmute": "Berhenti membisukan @{name}", - "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", - "column.blocks": "Pengguna diblokir", - "column.community": "Linimasa Lokal", - "column.favourites": "Favorit", - "column.follow_requests": "Permintaan mengikuti", - "column.home": "Beranda", - "column.mutes": "Pengguna dibisukan", - "column.notifications": "Notifikasi", - "column.public": "Linimasa gabunggan", - "column_back_button.label": "Kembali", - "column_subheading.navigation": "Navigasi", - "column_subheading.settings": "Pengaturan", - "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", - "compose_form.lock_disclaimer.lock": "dikunci", - "compose_form.placeholder": "Apa yang ada di pikiran anda?", - "compose_form.privacy_disclaimer": "Status pribadi anda akan dikirim ke pengguna yang disebut dalam {domains}. Apa anda mempercayai {domainsCount, plural, one {server tersebut} other {server tersebut}}? Privasi postingan hanya bekerja dalam server Mastodon. Jika {domains} {domainsCount, plural, one {bukan server Mastodon} other {bukan server Mastodon}}, akan ada indikasi bahwa postingan anda adalah postingan pribadi, dan dapat di-boost atau dapat dilihat oleh orang lain.", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Tandai media sensitif", - "compose_form.spoiler": "Sembunyikan teks dibalik peringatan", - "compose_form.spoiler_placeholder": "Peringatan konten", - "confirmation_modal.cancel": "Batal", - "confirmations.block.confirm": "Blokir", - "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?", - "confirmations.delete.confirm": "Hapus", - "confirmations.delete.message": "Apa anda yakin akan menghapus status ini?", - "confirmations.mute.confirm": "Bisukan", - "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", - "emoji_button.activity": "Aktivitas", - "emoji_button.flags": "Bendera", - "emoji_button.food": "Makanan & Minuman", - "emoji_button.label": "Tambahkan emoji", - "emoji_button.nature": "Alam", - "emoji_button.objects": "Benda-benda", - "emoji_button.people": "Orang", - "emoji_button.search": "Cari...", - "emoji_button.symbols": "Simbol", - "emoji_button.travel": "Tempat Wisata", - "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", - "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", - "empty_column.home.public_timeline": "linimasa publik", - "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", - "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", - "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual", - "follow_request.authorize": "Izinkan", - "follow_request.reject": "Tolak", - "getting_started.apps": "Tersedia dalam berbagai aplikasi", - "getting_started.heading": "Mulai", - "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat open source. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}. {apps}.", - "home.column_settings.advanced": "Tingkat Lanjut", - "home.column_settings.basic": "Dasar", - "home.column_settings.filter_regex": "Penyaringan dengan Regular Expression", - "home.column_settings.show_reblogs": "Tampilkan Boost", - "home.column_settings.show_replies": "Tampilkan balasan", - "home.settings": "Pengaturan kolom", - "lightbox.close": "Tutup", - "loading_indicator.label": "Tunggu sebentar...", - "media_gallery.toggle_visible": "Tampil/Sembunyikan", - "missing_indicator.label": "Tidak ditemukan", - "navigation_bar.blocks": "Pengguna diblokir", - "navigation_bar.community_timeline": "Linimasa lokal", - "navigation_bar.edit_profile": "Ubah profil", - "navigation_bar.favourites": "Favorit", - "navigation_bar.follow_requests": "Permintaan mengikuti", - "navigation_bar.info": "Informasi selengkapnya", - "navigation_bar.logout": "Keluar", - "navigation_bar.mutes": "Pengguna dibisukan", - "navigation_bar.preferences": "Pengaturan", - "navigation_bar.public_timeline": "Linimasa gabungan", - "notification.favourite": "{name} menyukai status anda", - "notification.follow": "{name} mengikuti anda", - "notification.reblog": "{name} mem-boost status anda", - "notifications.clear": "Hapus notifikasi", - "notifications.clear_confirmation": "Apa anda yakin hendak menghapus semua notifikasi anda?", - "notifications.column_settings.alert": "Notifikasi desktop", - "notifications.column_settings.favourite": "Favorit:", - "notifications.column_settings.follow": "Pengikut baru:", - "notifications.column_settings.mention": "Balasan:", - "notifications.column_settings.reblog": "Boost:", - "notifications.column_settings.show": "Tampilkan dalam kolom", - "notifications.column_settings.sound": "Mainkan suara", - "notifications.settings": "Pengaturan kolom", - "onboarding.done": "Selesei", - "onboarding.next": "Selanjutnya", - "onboarding.page_five.public_timelines": "Linimasa lokal menampilkan semua postingan publik dari semua orang di {domain}. Linimasa gabungan menampilkan postingan publik dari semua orang yang diikuti oleh {domain}. Ini semua adalah Linimasa Publik, cara terbaik untuk bertemu orang lain.", - "onboarding.page_four.home": "Linimasa beranda menampilkan postingan dari orang-orang yang anda ikuti.", - "onboarding.page_four.notifications": "Kolom notifikasi menampilkan ketika seseorang berinteraksi dengan anda.", - "onboarding.page_one.federation": "Mastodon adalah jaringan dari beberapa server independen yang bergabung untuk membuat jejaring sosial yang besar.", - "onboarding.page_one.handle": "Ada berada dalam {domain}, jadi nama user lengkap anda adalah {handle}", - "onboarding.page_one.welcome": "Selamat datang di Mastodon!", - "onboarding.page_six.admin": "Admin serveer anda adalah {admin}.", - "onboarding.page_six.almost_done": "Hampir selesei...", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "Ada beberapa apl yang tersedia untuk iOS, Android, dan platform lainnya.", - "onboarding.page_six.github": "Mastodon adalah software open-source. Anda bisa melaporkan bug, meminta fitur, atau berkontribusi dengan kode di {github}.", - "onboarding.page_six.guidelines": "pedoman komunitas", - "onboarding.page_six.read_guidelines": "Silakan baca {guidelines} {domain}!", - "onboarding.page_six.various_app": "apl handphone", - "onboarding.page_three.profile": "Ubah profil anda untuk mengganti avatar, bio, dan nama pengguna anda. Disitu, anda juga bisa mengatur opsi lainnya.", - "onboarding.page_three.search": "Gunakan kolom pencarian untuk mencari orang atau melihat hashtag, seperti {illustration} dan {introductions}. Untuk mencari pengguna yang tidak berada dalam server ini, gunakan nama pengguna mereka selengkapnya.", - "onboarding.page_two.compose": "Tulis postingan melalui kolom posting. Anda dapat mengunggah gambar, mengganti pengaturan privasi, dan menambahkan peringatan konten dengan ikon-ikon dibawah ini.", - "onboarding.skip": "Lewati", - "privacy.change": "Tentukan privasi status", - "privacy.direct.long": "Kirim hanya ke pengguna yang disebut", - "privacy.direct.short": "Langsung", - "privacy.private.long": "Kirim hanya ke pengikut", - "privacy.private.short": "Pribadi", - "privacy.public.long": "Kirim ke linimasa publik", - "privacy.public.short": "Publik", - "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", - "privacy.unlisted.short": "Tak Terdaftar", - "reply_indicator.cancel": "Batal", - "report.heading": "Laporan baru", - "report.placeholder": "Komentar tambahan", - "report.submit": "Kirim", - "report.target": "Melaporkan", - "search.status_by": "Status yang dibuat oleh {name}", - "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", - "status.cannot_reblog": "Postingan ini tidak dapat di-boost", - "search.placeholder": "Pencarian", - "search.status_by": "Status oleh {name}", - "status.delete": "Hapus", - "status.favourite": "Difavoritkan", - "status.load_more": "Tampilkan semua", - "status.media_hidden": "Media disembunyikan", - "status.mention": "Balasan @{name}", - "status.open": "Tampilkan status ini", - "status.reblog": "Boost", - "status.reblogged_by": "di-boost {name}", - "status.reply": "Balas", - "status.replyAll": "Balas ke semua", - "status.report": "Laporkan @{name}", - "status.sensitive_toggle": "Klik untuk menampilkan", - "status.sensitive_warning": "Konten sensitif", - "status.show_less": "Tampilkan lebih sedikit", - "status.show_more": "Tampilkan semua", - "tabs_bar.compose": "Tulis", - "tabs_bar.federated_timeline": "Gabungan", - "tabs_bar.home": "Beranda", - "tabs_bar.local_timeline": "Lokal", - "tabs_bar.notifications": "Notifikasi", - "upload_area.title": "Seret & lepaskan untuk mengunggah", - "upload_button.label": "Tambahkan media", - "upload_form.undo": "Undo", - "upload_progress.label": "Mengunggah...", - "video_player.toggle_sound": "Suara", - "video_player.toggle_visible": "Tampilan", - "video_player.expand": "Tampilkan video", - "video_player.video_error": "Video tidak dapat diputar", -}; - -export default id; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx @@ -1,57 +0,0 @@ -import ar from './ar'; -import en from './en'; -import de from './de'; -import es from './es'; -import fa from './fa'; -import he from './he'; -import hr from './hr'; -import hu from './hu'; -import io from './io'; -import it from './it'; -import fr from './fr'; -import nl from './nl'; -import no from './no'; -import oc from './oc'; -import pt from './pt'; -import pt_br from './pt-br'; -import uk from './uk'; -import fi from './fi'; -import eo from './eo'; -import ru from './ru'; -import ja from './ja'; -import zh_hk from './zh-hk'; -import zh_cn from './zh-cn'; -import bg from './bg'; -import id from './id'; - -const locales = { - ar, - en, - de, - es, - fa, - he, - hr, - hu, - io, - it, - fr, - nl, - no, - oc, - pt, - 'pt-BR': pt_br, - uk, - fi, - eo, - ru, - ja, - 'zh-HK': zh_hk, - 'zh-CN': zh_cn, - bg, - id, -}; - -export default function getMessagesForLocale (locale) { - return locales[locale]; -}; diff --git a/app/assets/javascripts/components/locales/io.jsx b/app/assets/javascripts/components/locales/io.jsx @@ -1,126 +0,0 @@ -const io = { - "account.block": "Blokusar @{name}", - "account.disclaimer": "Ca uzero esas de altra instaluro. Ca nombro forsan esas plu granda.", - "account.edit_profile": "Modifikar profilo", - "account.follow": "Sequar", - "account.followers": "Sequanti", - "account.follows_you": "Sequas tu", - "account.follows": "Sequas", - "account.mention": "Mencionar @{name}", - "account.mute": "Celar @{name}", - "account.posts": "Mesaji", - "account.report": "Denuncar @{name}", - "account.requested": "Vartante aprobo", - "account.unblock": "Desblokusar @{name}", - "account.unfollow": "Ne plus sequar", - "account.unmute": "Ne plus celar @{name}", - "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", - "column_back_button.label": "Retro", - "column.blocks": "Blokusita uzeri", - "column.community": "Lokala tempolineo", - "column.favourites": "Favorati", - "column.follow_requests": "Demandi di sequado", - "column.home": "Hemo", - "column.mutes": "Celita uzeri", - "column.notifications": "Savigi", - "column.public": "Federata tempolineo", - "compose_form.placeholder": "Quo esas en tua spirito?", - "compose_form.privacy_disclaimer": "Tua privata mesajo livresos a mencionata uzeri en {domains}. Ka tu fidas {domainsCount, plural, one {ta servero} other {ta serveri}}? Privateso di mesaji funcionas nur en instaluri di Mastodon. Se {domains} {domainsCount, plural, one {ne esas instaluro di Mastodon} other {ne esas instaluri di Mastodon}}, esos nula indiko, ke tua mesajo esas privata, ed ol povos repetesar od altre divenar videbla da nedezirinda recevanti.", - "compose_form.publish": "Siflar", - "compose_form.sensitive": "Markizar kontenajo kom trubliva", - "compose_form.spoiler_placeholder": "Averto di kontenajo", - "compose_form.spoiler": "Celar texto dop averto", - "emoji_button.label": "Insertar emoji", - "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!", - "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.", - "empty_column.home.public_timeline": "la publika tempolineo", - "empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.", - "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.", - "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.", - "follow_request.authorize": "Yurizar", - "follow_request.reject": "Refuzar", - "getting_started.apps": "Apliki diversa esas disponebla", - "getting_started.heading": "Debuto", - "getting_started.open_source_notice": "Mastodon esas programaro kun apertita kodexo. Tu povas kontributar o signalar problemi en GitHub ye {github}. {apps}.", - "home.column_settings.advanced": "Komplexa", - "home.column_settings.basic": "Simpla", - "home.column_settings.filter_regex": "Ekfiltrar per reguloza expresuri", - "home.column_settings.show_reblogs": "Montrar repeti", - "home.column_settings.show_replies": "Montrar respondi", - "home.settings": "Aranji di la kolumno", - "lightbox.close": "Klozar", - "loading_indicator.label": "Kargante...", - "media_gallery.toggle_visible": "Chanjar videbleso", - "missing_indicator.label": "Ne trovita", - "navigation_bar.blocks": "Blokusita uzeri", - "navigation_bar.community_timeline": "Lokala tempolineo", - "navigation_bar.edit_profile": "Modifikar profilo", - "navigation_bar.favourites": "Favorati", - "navigation_bar.follow_requests": "Demandi di sequado", - "navigation_bar.info": "Detaloza informi", - "navigation_bar.logout": "Ekirar", - "navigation_bar.mutes": "Celita uzeri", - "navigation_bar.preferences": "Preferi", - "navigation_bar.public_timeline": "Federata tempolineo", - "notification.favourite": "{name} favorizis tua mesajo", - "notification.follow": "{name} sequeskis tu", - "notification.mention": "{name} mencionis tu", - "notification.reblog": "{name} repetis tua mesajo", - "notifications.clear_confirmation": "Ka tu esas certa, ke tu volas efacar omna tua savigi?", - "notifications.clear": "Efacar savigi", - "notifications.column_settings.alert": "Surtabla savigi", - "notifications.column_settings.favourite": "Favorati:", - "notifications.column_settings.follow": "Nova sequanti:", - "notifications.column_settings.mention": "Mencioni:", - "notifications.column_settings.reblog": "Repeti:", - "notifications.column_settings.show": "Montrar en kolumno", - "notifications.column_settings.sound": "Plear sono", - "notifications.settings": "Aranji di kolumno", - "privacy.change": "Aranjar privateso di mesaji", - "privacy.direct.long": "Sendar nur a mencionata uzeri", - "privacy.direct.short": "Direte", - "privacy.private.long": "Sendar nur a sequanti", - "privacy.private.short": "Private", - "privacy.public.long": "Sendar a publika tempolinei", - "privacy.public.short": "Publike", - "privacy.unlisted.long": "Ne montrar en publika tempolinei", - "privacy.unlisted.short": "Ne enlistigota", - "reply_indicator.cancel": "Nihiligar", - "report.heading": "Nova denunco", - "report.placeholder": "Plusa komenti", - "report.submit": "Sendar", - "report.target": "Denuncante", - "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", - "search.placeholder": "Serchez", - "search.status_by": "Mesajo da {name}", - "status.delete": "Efacar", - "status.favourite": "Favorizar", - "status.load_more": "Kargar pluse", - "status.media_hidden": "Kontenajo celita", - "status.mention": "Mencionar @{name}", - "status.open": "Detaligar ca mesajo", - "status.reblog": "Repetar", - "status.reblogged_by": "{name} repetita", - "status.reply": "Respondar", - "status.replyAll": "Respondar a filo", - "status.report": "Denuncar @{name}", - "status.sensitive_toggle": "Kliktar por vidar", - "status.sensitive_warning": "Trubliva kontenajo", - "status.show_less": "Montrar mine", - "status.show_more": "Montrar plue", - "tabs_bar.compose": "Kompozar", - "tabs_bar.federated_timeline": "Federata", - "tabs_bar.home": "Hemo", - "tabs_bar.local_timeline": "Lokala", - "tabs_bar.notifications": "Savigi", - "upload_area.title": "Tranar faligar por kargar", - "upload_button.label": "Adjuntar kontenajo", - "upload_form.undo": "Desfacar", - "upload_progress.label": "Kargante...", - "video_player.toggle_sound": "Acendar sono", - "video_player.toggle_visible": "Chanjar videbleso", - "video_player.expand": "Extensar video", - "video_player.video_error": "Video ne povus pleesar", -}; - -export default io; diff --git a/app/assets/javascripts/components/locales/it.jsx b/app/assets/javascripts/components/locales/it.jsx @@ -1,125 +0,0 @@ -const it = { - "account.block": "Blocca @{name}", - "account.disclaimer": "Questo utente si trova su un altro server. Questo numero potrebbe essere maggiore.", - "account.edit_profile": "Modifica profilo", - "account.follow": "Segui", - "account.followers": "Seguaci", - "account.follows_you": "Ti segue", - "account.follows": "Segue", - "account.mention": "Menziona @{name}", - "account.mute": "Silenzia @{name}", - "account.posts": "Posts", - "account.report": "Segnala @{name}", - "account.requested": "In attesa di approvazione", - "account.unblock": "Sblocca @{name}", - "account.unfollow": "Non seguire", - "account.unmute": "Non silenziare @{name}", - "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", - "column_back_button.label": "Indietro", - "column.blocks": "Utenti bloccati", - "column.community": "Timeline locale", - "column.favourites": "Apprezzati", - "column.follow_requests": "Richieste di amicizia", - "column.home": "Home", - "column.mutes": "Utenti silenziati", - "column.notifications": "Notifiche", - "column.public": "Timeline federata", - "compose_form.placeholder": "A cosa stai pensando?", - "compose_form.privacy_disclaimer": "Il tuo status privato verrà condiviso con gli utenti menzionati su {domains}. Ti fidi di {domainsCount, plural, one {quel server} other {quei server}}? Le impostazioni sulla privacy valgono solo su server Mastodon. Se {domains} {domainsCount, plural, one {non è un server Mastodon} other {non sono server Mastodon}}, non ci saranno indicazioni sulla privacy del tuo status, e potrebbe essere condiviso o reso visibile a destinatari indesiderati.", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Segnala file come sensibile", - "compose_form.spoiler_placeholder": "Content warning", - "compose_form.spoiler": "Nascondi testo con avvertimento", - "emoji_button.label": "Inserisci emoji", - "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", - "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.", - "empty_column.home.public_timeline": "la timeline pubblica", - "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.", - "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.", - "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.", - "follow_request.authorize": "Autorizza", - "follow_request.reject": "Rifiuta", - "getting_started.apps": "Sono disponibili diverse app", - "getting_started.heading": "Come iniziare", - "getting_started.open_source_notice": "Mastodon è un software open source. Puoi contribuire o segnalare errori su GitHub all'indirizzo {github}. {apps}.", - "home.column_settings.advanced": "Avanzato", - "home.column_settings.basic": "Semplice", - "home.column_settings.filter_regex": "Filtra con espressioni regolari", - "home.column_settings.show_reblogs": "Mostra post condivisi", - "home.column_settings.show_replies": "Mostra risposte", - "home.settings": "Impostazioni colonna", - "lightbox.close": "Chiudi", - "loading_indicator.label": "Carico...", - "media_gallery.toggle_visible": "Imposta visibilità", - "missing_indicator.label": "Non trovato", - "navigation_bar.blocks": "Utenti bloccati", - "navigation_bar.community_timeline": "Timeline locale", - "navigation_bar.edit_profile": "Modifica profilo", - "navigation_bar.favourites": "Apprezzati", - "navigation_bar.follow_requests": "Richieste di amicizia", - "navigation_bar.info": "Informazioni estese", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Utenti silenziati", - "navigation_bar.preferences": "Impostazioni", - "navigation_bar.public_timeline": "Timeline federata", - "notification.favourite": "{name} ha apprezzato il tuo post", - "notification.follow": "{name} ha iniziato a seguirti", - "notification.mention": "{name} ti ha menzionato", - "notification.reblog": "{name} ha condiviso il tuo post", - "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?", - "notifications.clear": "Cancella notifiche", - "notifications.column_settings.alert": "Notifiche desktop", - "notifications.column_settings.favourite": "Apprezzati:", - "notifications.column_settings.follow": "Nuovi seguaci:", - "notifications.column_settings.mention": "Menzioni:", - "notifications.column_settings.reblog": "Post condivisi:", - "notifications.column_settings.show": "Mostra in colonna", - "notifications.column_settings.sound": "Riproduci suono", - "notifications.settings": "Impostazioni colonna", - "privacy.change": "Modifica privacy post", - "privacy.direct.long": "Invia solo a utenti menzionati", - "privacy.direct.short": "Diretto", - "privacy.private.long": "Invia solo ai seguaci", - "privacy.private.short": "Privato", - "privacy.public.long": "Invia alla timeline pubblica", - "privacy.public.short": "Pubblico", - "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", - "privacy.unlisted.short": "Non elencato", - "reply_indicator.cancel": "Annulla", - "report.heading": "Nuova segnalazione", - "report.placeholder": "Commenti aggiuntivi", - "report.submit": "Invia", - "report.target": "Invio la segnalazione", - "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", - "search.placeholder": "Cerca", - "search.status_by": "Status per {name}", - "status.delete": "Elimina", - "status.favourite": "Apprezzato", - "status.load_more": "Mostra di più", - "status.media_hidden": "Allegato nascosto", - "status.mention": "Nomina @{name}", - "status.open": "Espandi questo post", - "status.reblog": "Condividi", - "status.reblogged_by": "{name} ha condiviso", - "status.reply": "Rispondi", - "status.report": "Segnala @{name}", - "status.sensitive_toggle": "Clicca per vedere", - "status.sensitive_warning": "Materiale sensibile", - "status.show_less": "Mostra meno", - "status.show_more": "Mostra di più", - "tabs_bar.compose": "Scrivi", - "tabs_bar.federated_timeline": "Federazione", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Locale", - "tabs_bar.notifications": "Notifiche", - "upload_area.title": "Trascina per caricare", - "upload_button.label": "Aggiungi file multimediale", - "upload_form.undo": "Annulla", - "upload_progress.label": "Sto caricando...", - "video_player.toggle_sound": "Attiva suono", - "video_player.toggle_visible": "Attiva visibilità", - "video_player.expand": "Espandi video", - "video_player.video_error": "Il video non può essere riprodotto", -}; - -export default it;- \ No newline at end of file diff --git a/app/assets/javascripts/components/locales/ja.jsx b/app/assets/javascripts/components/locales/ja.jsx @@ -1,167 +0,0 @@ -const ja = { - "account.block": "ブロック", - "account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。", - "account.edit_profile": "プロフィールを編集", - "account.follow": "フォロー", - "account.followers": "フォロワー", - "account.follows": "フォロー", - "account.follows_you": "フォローされています", - "account.mention": "返信", - "account.mute": "ミュート", - "account.posts": "投稿", - "account.report": "通報", - "account.requested": "承認待ち", - "account.unblock": "ブロック解除", - "account.unfollow": "フォロー解除", - "account.unmute": "ミュート解除", - "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", - "column.blocks": "ブロックしたユーザー", - "column.community": "ローカルタイムライン", - "column.favourites": "お気に入り", - "column.follow_requests": "フォローリクエスト", - "column.home": "ホーム", - "column.mutes": "ミュートしたユーザー", - "column.notifications": "通知", - "column.public": "連合タイムライン", - "column_back_button.label": "戻る", - "column_subheading.navigation": "ナビゲーション", - "column_subheading.settings": "設定", - "compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。", - "compose_form.lock_disclaimer.lock": "非公開", - "compose_form.placeholder": "今なにしてる?", - "compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先ユーザーが所属する {domains} に送信されます。{domainsCount, plural, one {このサーバー} other {これらのサーバー}}は信頼できますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 {domains} {domainsCount, plural, one {がMastodonインスタンス} other {がMastodonインスタンス}}でない場合、あなたの投稿がプライベートなものとして扱われず、ブーストされたり予期しないユーザーに見られる可能性があります。", - "compose_form.publish": "トゥート", - "compose_form.sensitive": "メディアを閲覧注意としてマークする", - "compose_form.spoiler": "テキストを隠す", - "compose_form.spoiler_placeholder": "警告", - "confirmation_modal.cancel": "キャンセル", - "confirmations.block.confirm": "ブロック", - "confirmations.block.message": "本当に {name} をブロックしますか?", - "confirmations.delete.confirm": "削除", - "confirmations.delete.message": "本当に削除しますか?", - "confirmations.mute.confirm": "ミュート", - "confirmations.mute.message": "本当に {name} をミュートしますか?", - "emoji_button.label": "絵文字を追加", - "emoji_button.search": "検索...", - "emoji_button.people": "人々", - "emoji_button.nature": "自然", - "emoji_button.food": "食べ物", - "emoji_button.activity": "活動", - "emoji_button.travel": "旅行と場所", - "emoji_button.objects": "物", - "emoji_button.symbols": "記号", - "emoji_button.flags": "国旗", - "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!", - "empty_column.hashtag": "このハッシュタグはまだ使われていません。", - "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。", - "empty_column.home.public_timeline": "連合タイムライン", - "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", - "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!", - "follow_request.authorize": "許可", - "follow_request.reject": "拒否", - "getting_started.apps": "さまざまなアプリで利用できます。", - "getting_started.heading": "スタート", - "getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}", - "home.column_settings.advanced": "上級者向け", - "home.column_settings.basic": "シンプル", - "home.column_settings.filter_regex": "正規表現でフィルター", - "home.column_settings.show_reblogs": "ブースト表示", - "home.column_settings.show_replies": "返信表示", - "home.settings": "カラム設定", - "lightbox.close": "閉じる", - "loading_indicator.label": "読み込み中...", - "media_gallery.toggle_visible": "表示切り替え", - "missing_indicator.label": "見つかりません", - "navigation_bar.blocks": "ブロックしたユーザー", - "navigation_bar.community_timeline": "ローカルタイムライン", - "navigation_bar.edit_profile": "プロフィールを編集", - "navigation_bar.favourites": "お気に入り", - "navigation_bar.follow_requests": "フォローリクエスト", - "navigation_bar.info": "サーバー情報", - "navigation_bar.logout": "ログアウト", - "navigation_bar.mutes": "ミュートしたユーザー", - "navigation_bar.preferences": "ユーザー設定", - "navigation_bar.public_timeline": "連合タイムライン", - "notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました", - "notification.follow": "{name} さんにフォローされました", - "notification.mention": "{name} さんがあなたに返信しました", - "notification.reblog": "{name} さんがあなたのトゥートをブーストしました", - "notifications.clear": "通知を消去", - "notifications.clear_confirmation": "本当に通知を消去しますか?", - "notifications.column_settings.alert": "デスクトップ通知", - "notifications.column_settings.favourite": "お気に入り", - "notifications.column_settings.follow": "新しいフォロワー", - "notifications.column_settings.mention": "返信", - "notifications.column_settings.reblog": "ブースト", - "notifications.column_settings.show": "カラムに表示", - "notifications.column_settings.sound": "通知音を再生", - "notifications.settings": "カラム設定", - "onboarding.done": "完了", - "onboarding.next": "次へ", - "onboarding.page_one.welcome": "Mastodonへようこそ!", - "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。", - "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。", - "onboarding.page_two.compose": "フォームから投稿できます。イメージや、公開範囲の設定や、表示時の警告の設定は下部のアイコンから行なえます。", - "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。", - "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。", - "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。", - "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。", - "onboarding.page_five.public_timelines": "連合タイムラインでは{domain}の人がフォローしているMastodon全体での公開投稿を表示します。同じくローカルタイムラインでは{domain}のみの公開投稿を表示します。", - "onboarding.page_six.almost_done": "以上です。", - "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。", - "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。", - "onboarding.page_six.guidelines": "コミュニティガイドライン", - "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。", - "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。", - "onboarding.page_six.various_app": "様々なモバイルアプリ", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.skip": "スキップ", - "privacy.change": "投稿のプライバシーを変更", - "privacy.direct.long": "メンションしたユーザーだけに公開", - "privacy.direct.short": "ダイレクト", - "privacy.private.long": "フォロワーだけに公開", - "privacy.private.short": "非公開", - "privacy.public.long": "公開TLに投稿する", - "privacy.public.short": "公開", - "privacy.unlisted.long": "公開TLで表示しない", - "privacy.unlisted.short": "未収載", - "reply_indicator.cancel": "キャンセル", - "report.heading": "新規通報", - "report.placeholder": "コメント", - "report.submit": "通報する", - "report.target": "問題のユーザー", - "search.placeholder": "検索", - "search.status_by": "{name}からの投稿", - "search_results.total": "{count, number} 件の結果", - "status.cannot_reblog": "この投稿はブーストできません", - "status.delete": "削除", - "status.favourite": "お気に入り", - "status.load_more": "もっと見る", - "status.media_hidden": "非表示のメデイア", - "status.mention": "返信", - "status.open": "詳細を表示", - "status.reblog": "ブースト", - "status.reblogged_by": "{name} さんにブーストされました", - "status.reply": "返信", - "status.replyAll": "全員に返信", - "status.report": "通報", - "status.sensitive_toggle": "クリックして表示", - "status.sensitive_warning": "閲覧注意", - "status.show_less": "隠す", - "status.show_more": "もっと見る", - "tabs_bar.compose": "投稿", - "tabs_bar.federated_timeline": "連合", - "tabs_bar.home": "ホーム", - "tabs_bar.local_timeline": "ローカル", - "tabs_bar.notifications": "通知", - "upload_area.title": "ドラッグ&ドロップでアップロード", - "upload_button.label": "メディアを追加", - "upload_form.undo": "やり直す", - "upload_progress.label": "アップロード中…", - "video_player.expand": "動画の詳細", - "video_player.toggle_sound": "音の切り替え", - "video_player.toggle_visible": "表示切り替え", - "video_player.video_error": "動画の再生に失敗しました", -}; - -export default ja; diff --git a/app/assets/javascripts/components/locales/nl.jsx b/app/assets/javascripts/components/locales/nl.jsx @@ -1,130 +0,0 @@ -const nl = { - "account.block": "Blokkeer @{name}", - "account.edit_profile": "Profiel bewerken", - "account.followers": "Volgers", - "account.follows": "Volgt", - "account.follows_you": "Volgt jou", - "account.follow": "Volgen", - "account.mention": "Vermeld @{name}", - "account.mute": "Negeer @{name}", - "account.posts": "Berichten", - "account.report": "Rapporteer @{name}", - "account.requested": "Wacht op goedkeuring", - "account.unblock": "Deblokkeer @{name}", - "account.unfollow": "Ontvolgen", - "account.unmute": "Negeer @{name} niet meer", - "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", - "column_back_button.label": "terug", - "column.blocks": "Geblokkeerde gebruikers", - "column.community": "Lokale tijdlijn", - "column.favourites": "Favorieten", - "column.home": "Jouw tijdlijn", - "column.mutes": "Genegeerde gebruikers", - "column.notifications": "Meldingen", - "column.public": "Globale tijdlijn", - "column_subheading.navigation": "Navigatie", - "column_subheading.settings": "Instellingen", - "compose_form.placeholder": "Wat wil je kwijt?", - "compose_form.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {die server} other {die servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Wanneer {domains} {domainsCount, plural, one {geen Mastodon-server is} other {geen Mastodon-servers zijn}}, dan wordt er niet aangegeven dat de toot privé is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.", - "compose_form.private": "Als privé markeren", - "compose_form.publish": "Toot", - "compose_form.sensitive": "Media als gevoelig markeren", - "compose_form.spoiler_placeholder": "Waarschuwingstekst", - "compose_form.spoiler": "Tekst achter waarschuwing verbergen", - "compose_form.unlisted": "Niet op openbare tijdlijnen tonen", - "emoji_button.activity": "Activiteiten", - "emoji_button.flags": "Vlaggen", - "emoji_button.food": "Eten en drinken", - "emoji_button.label": "Emoji toevoegen", - "emoji_button.nature": "Natuur", - "emoji_button.objects": "Voorwerpen", - "emoji_button.people": "Mensen", - "emoji_button.search": "Zoeken...", - "emoji_button.symbols": "Symbolen", - "emoji_button.travel": "Reizen en plekken", - "getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent. Voer hiervoor het e-mailachtige adres in het zoekveld in.", - "getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in toots wilt vermelden.", - "getting_started.apps": "Er zijn meerdere apps beschikbaar", - "getting_started.heading": "Beginnen", - "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.", - "lightbox.close": "Sluiten", - "loading_indicator.label": "Laden…", - "navigation_bar.blocks": "Geblokkeerde gebruikers", - "navigation_bar.community_timeline": "Lokale tijdlijn", - "navigation_bar.edit_profile": "Profiel bewerken", - "navigation_bar.favourites": "Favorieten", - "navigation_bar.follow_requests": "Volgverzoeken", - "navigation_bar.info": "Uitgebreide informatie", - "navigation_bar.logout": "Afmelden", - "navigation_bar.mutes": "Genegeerde gebruikers", - "navigation_bar.preferences": "Instellingen", - "navigation_bar.public_timeline": "Globale tijdlijn", - "notification.favourite": "{name} markeerde jouw toot als favoriet", - "notification.follow": "{name} volgt jou nu", - "notification.mention": "{name} vermeldde jou", - "notification.reblog": "{name} boostte jouw toot", - "notifications.clear_confirmation": "Weet je zeker dat je al jouw meldingen wilt verwijderen?", - "notifications.clear": "Meldingen verwijderen", - "notifications.column_settings.alert": "Desktopmeldingen", - "notifications.column_settings.favourite": "Favorieten:", - "notifications.column_settings.follow": "Nieuwe volgers:", - "notifications.column_settings.mention": "Vermeldingen:", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "In kolom tonen", - "notifications.column_settings.sound": "Geluid afspelen", - "notifications.settings": "Kolom-instellingen", - "onboarding.next": "Volgende", - "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", - "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.", - "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.", - "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.", - "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}", - "onboarding.page_one.welcome": "Welkom op Mastodon!", - "onboarding.page_six.admin": "De beheerder van jouw Mastodon-server is {admin}.", - "onboarding.page_six.almost_done": "Bijna klaar...", - "onboarding.page_six.appetoot": "Veel succes!", - "onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.", - "onboarding.page_six.github": "Mastodon kost niets, en is open-source- en vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.", - "onboarding.page_six.guidelines": "communityrichtlijnen", - "onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!", - "onboarding.page_six.various_app": "mobiele apps", - "onboarding.page_three.profile": "Bewerk jouw profiel om jouw avatar, bio en weergavenaam te veranderen. Daar vind je ook andere instellingen.", - "onboarding.page_three.search": "Gebruik de zoekbalk linksboven om andere mensen op Mastodon te vinden en om te zoeken op hashtags, zoals {illustration} en {introductions}. Om iemand te vinden die niet op deze Mastodon-server zit, moet je het volledige Mastodon-adres van deze persoon invoeren.", - "onboarding.page_two.compose": "Schrijf berichten (wij noemen dit toots) in het tekstvak in de linkerkolom. Je kan met de pictogrammen daaronder afbeeldingen uploaden, privacy-instellingen veranderen en je tekst een waarschuwing meegeven.", - "onboarding.skip": "Overslaan", - "privacy.change": "Privacy toot aanpassen", - "privacy.direct.long": "Toot alleen naar vermelde gebruikers", - "privacy.direct.short": "Direct", - "privacy.private.long": "Alleen aan volgers tonen", - "privacy.private.short": "Alleen volgers", - "privacy.public.long": "Op openbare tijdlijnen tonen", - "privacy.public.short": "Openbaar", - "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", - "privacy.unlisted.short": "Minder openbaar", - "reply_indicator.cancel": "Annuleren", - "search.account": "Account", - "search.hashtag": "Hashtag", - "search.placeholder": "Zoeken", - "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", - "status.delete": "Verwijderen", - "status.favourite": "Favoriet", - "status.mention": "@{name} vermelden", - "status.reblog": "Boost", - "status.reblogged_by": "{name} boostte", - "status.reply": "Reageren", - "status.sensitive_toggle": "Klik om te zien", - "status.sensitive_warning": "Gevoelige inhoud", - "status.show_less": "Minder tonen", - "status.show_more": "Meer tonen", - "tabs_bar.compose": "Schrijven", - "tabs_bar.home": "Jouw tijdlijn", - "tabs_bar.mentions": "Vermeldingen", - "tabs_bar.notifications": "Meldingen", - "tabs_bar.public": "Globale tijdlijn", - "upload_button.label": "Media toevoegen", - "upload_form.undo": "Ongedaan maken", - "video_player.toggle_sound": "Geluid in-/uitschakelen", - -}; - -export default nl; diff --git a/app/assets/javascripts/components/locales/no.jsx b/app/assets/javascripts/components/locales/no.jsx @@ -1,130 +0,0 @@ -const no = { - "account.block": "Blokkér @{name}", - "account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.", - "account.edit_profile": "Rediger profil", - "account.follow": "Følg", - "account.followers": "Følgere", - "account.follows_you": "Følger deg", - "account.follows": "Følger", - "account.mention": "Nevn @{name}", - "account.mute": "Demp @{name}", - "account.posts": "Innlegg", - "account.report": "Rapportér @{name}", - "account.requested": "Venter på godkjennelse", - "account.unblock": "Avblokker @{name}", - "account.unfollow": "Avfølg", - "account.unmute": "Avdemp @{name}", - "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", - "column_back_button.label": "Tilbake", - "column.blocks": "Blokkerte brukere", - "column.community": "Lokal tidslinje", - "column.favourites": "Likt", - "column.follow_requests": "Følgeforespørsler", - "column.home": "Hjem", - "column.notifications": "Varslinger", - "column.public": "Felles tidslinje", - "compose_form.placeholder": "Hva har du på hjertet?", - "compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.", - "compose_form.publish": "Tut", - "compose_form.sensitive": "Merk media som følsomt", - "compose_form.spoiler_placeholder": "Innholdsadvarsel", - "compose_form.spoiler": "Skjul tekst bak advarsel", - "emoji_button.label": "Sett inn emoji", - "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!", - "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.", - "empty_column.home.public_timeline": "en offentlig tidslinje", - "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.", - "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.", - "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp", - "follow_request.authorize": "Autorisér", - "follow_request.reject": "Avvis", - "getting_started.apps": "Diverse apper er tilgjengelige", - "getting_started.heading": "Kom i gang", - "getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.", - "home.column_settings.advanced": "Advansert", - "home.column_settings.basic": "Enkel", - "home.column_settings.filter_regex": "Filtrér med regulære uttrykk", - "home.column_settings.show_reblogs": "Vis fremhevinger", - "home.column_settings.show_replies": "Vis svar", - "home.settings": "Kolonneinnstillinger", - "lightbox.close": "Lukk", - "loading_indicator.label": "Laster...", - "media_gallery.toggle_visible": "Veksle synlighet", - "missing_indicator.label": "Ikke funnet", - "navigation_bar.blocks": "Blokkerte brukere", - "navigation_bar.community_timeline": "Lokal tidslinje", - "navigation_bar.edit_profile": "Rediger profil", - "navigation_bar.favourites": "Likt", - "navigation_bar.follow_requests": "Følgeforespørsler", - "navigation_bar.info": "Utvidet informasjon", - "navigation_bar.logout": "Logg ut", - "navigation_bar.preferences": "Preferanser", - "navigation_bar.public_timeline": "Felles tidslinje", - "notification.favourite": "{name} likte din status", - "notification.follow": "{name} fulgte deg", - "notification.reblog": "{name} fremhevde din status", - "notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?", - "notifications.clear": "Fjern varsler", - "notifications.column_settings.alert": "Skrivebordsvarslinger", - "notifications.column_settings.favourite": "Likt:", - "notifications.column_settings.follow": "Nye følgere:", - "notifications.column_settings.mention": "Nevninger:", - "notifications.column_settings.reblog": "Fremhevinger:", - "notifications.column_settings.show": "Vis i kolonne", - "notifications.column_settings.sound": "Spill lyd", - "notifications.settings": "Kolonneinstillinger", - "privacy.change": "Justér synlighet", - "privacy.direct.long": "Post kun til nevnte brukere", - "privacy.direct.short": "Direkte", - "privacy.private.long": "Post kun til følgere", - "privacy.private.short": "Privat", - "privacy.public.long": "Post kun til offentlige tidslinjer", - "privacy.public.short": "Offentlig", - "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", - "privacy.unlisted.short": "Uoppført", - "reply_indicator.cancel": "Avbryt", - "report.heading": "Ny rapport", - "report.placeholder": "Tilleggskommentarer", - "report.submit": "Send inn", - "report.target": "Rapporterer", - "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", - "search.placeholder": "Søk", - "search.status_by": "Status fra {name}", - "status.delete": "Slett", - "status.favourite": "Lik", - "status.load_more": "Last mer", - "status.media_hidden": "Media skjult", - "status.mention": "Nevn @{name}", - "status.open": "Utvid denne statusen", - "status.reblog": "Fremhev", - "status.reblogged_by": "Fremhevd av {name}", - "status.reply": "Svar", - "status.report": "Rapporter @{name}", - "status.sensitive_toggle": "Klikk for å vise", - "status.sensitive_warning": "Følsomt innhold", - "status.show_less": "Vis mindre", - "status.show_more": "Vis mer", - "tabs_bar.compose": "Komponer", - "tabs_bar.federated_timeline": "Felles", - "tabs_bar.home": "Hjem", - "tabs_bar.local_timeline": "Lokal", - "tabs_bar.notifications": "Varslinger", - "upload_area.title": "Dra og slipp for å laste opp", - "upload_button.label": "Legg til media", - "upload_form.undo": "Angre", - "upload_progress.label": "Laster opp...", - "video_player.toggle_sound": "Veksle lyd", - "video_player.toggle_visible": "Veksle synlighet", - "video_player.expand": "Utvid video", - "getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.", - "getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.", - "tabs_bar.mentions": "Nevninger", - "tabs_bar.public": "Felles tidslinje", - "compose_form.private": "Merk som privat", - "compose_form.unlisted": "Ikke vis på offentlige tidslinjer", - "search.account": "Konto", - "search.hashtag": "Hashtag", - "notification.mention": "{name} nevnte deg" -}; - -export default no; diff --git a/app/assets/javascripts/components/locales/oc.jsx b/app/assets/javascripts/components/locales/oc.jsx @@ -1,128 +0,0 @@ -const oc = { - "column_back_button.label": "Tornar", - "lightbox.close": "Tampar", - "loading_indicator.label": "Cargament…", - "status.mention": "Mencionar", - "status.delete": "Escafar", - "status.reply": "Respondre", - "status.reblog": "Partejar", - "status.favourite": "Apondre als favorits", - "status.reblogged_by": "{name} a partejat :", - "status.sensitive_warning": "Contengut embarrassant", - "status.sensitive_toggle": "Clicar per mostrar", - "status.show_more": "Desplegar", - "status.show_less": "Tornar plegar", - "status.open": "Desplegar aqueste estatut", - "status.report": "Senhalar @{name}", - "status.load_more": "Cargar mai", - "status.media_hidden": "Mèdia rescondut", - "video_player.toggle_sound": "Activar/Desactivar lo son", - "video_player.toggle_visible": "Mostrar/Rescondre la vidèo", - "account.mention": "Mencionar", - "account.edit_profile": "Modificar lo perfil", - "account.unblock": "Desblocar", - "account.unfollow": "Quitar de sègre", - "account.block": "Blocar", - "account.mute": "Rescondre", - "account.unmute": "Quitar de rescondre", - "account.follow": "Sègre", - "account.posts": "Estatuts", - "account.follows": "Abonaments", - "account.followers": "Abonats", - "account.follows_you": "Vos sèc", - "account.requested": "Invitacion mandada", - "account.report": "Senhalar", - "account.disclaimer": "Aqueste compte es sus una autra instància. Los nombres pòdon èsser mai grandes.", - "getting_started.heading": "Per començar", - "getting_started.about_addressing": "Podètz sègre los estatuts de qualqu’un en picant son identificant e lo domeni de l’instància separat amb un @ coma una adreça de corrièl dins lo camp de recèrca.", - "getting_started.about_shortcuts": "S’aquesta persona emplega la meteissa instància que vos l’identifican basta. Atal foncionan tanben las mencions dins vòstres estatuts.", - "getting_started.about_developer": "Per sègre lo desvolopaire d’aqueste projècte : Gargron@mastodon.social", - "getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via{github} sus GitHub.", - "column.home": "Acuèlh", - "column.community": "Fil public local", - "column.public": "Fil public global", - "column.notifications": "Notificacions", - "column.blocks": "Personas blocadas", - "column.favourites": "Favorits", - "column.follow_requests": "Demandas d’abonament", - "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", - "empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo fil public.", - "empty_column.home": "Pel moment segètz pas segun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.", - "empty_column.home.public_timeline": "lo fil public", - "empty_column.community": "Lo fil public local es void. Escribètz quicòm per lo garnir !", - "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", - "tabs_bar.compose": "Compausar", - "tabs_bar.home": "Acuèlh", - "tabs_bar.mentions": "Mencions", - "tabs_bar.public": "Fil public global", - "tabs_bar.notifications": "Notifications", - "tabs_bar.local_timeline": "Fil public local", - "tabs_bar.federated_timeline": "Fil public global", - "compose_form.placeholder": "A de qué pensatz ?", - "compose_form.publish": "Tut", - "compose_form.sensitive": "Marcar lo mèdia coma embarrassant", - "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", - "compose_form.spoiler_placeholder": "Avertiment", - "compose_form.private": "Far venir privat", - "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", - "compose_form.unlisted": "Mostrar pas dins los fils publics", - "emoji_button.label": "Inserir un emoji", - "navigation_bar.edit_profile": "Modificar lo perfil", - "navigation_bar.preferences": "Preferéncias", - "navigation_bar.community_timeline": "Fil public local", - "navigation_bar.public_timeline": "Fil public global", - "navigation_bar.blocks": "Personas blocadas", - "navigation_bar.favourites": "Favorits", - "navigation_bar.info": "Mai informacions", - "navigation_bar.logout": "Desconnexion", - "navigation_bar.follow_requests": "Demandas d'abonament", - "reply_indicator.cancel": "Anullar", - "search.placeholder": "Recercar", - "search.account": "Compte", - "search.hashtag": "Mot-clau", - "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", - "search.status_by": "Estatuts de {name}", - "upload_button.label": "Apondre un mèdia", - "upload_form.undo": "Anullar", - "upload_progress.label": "Mandadís…", - "upload_area.title": "Lisatz e depausatz per mandar", - "notification.follow": "{name} vos sèc.", - "notification.favourite": "{name} a apondut a sos favorits :", - "notification.reblog": "{name} a partejat vòstre estatut :", - "notification.mention": "{name} vos a mencionat :", - "notifications.column_settings.alert": "Notificacions localas", - "notifications.column_settings.show": "Mostrar dins la colomna", - "notifications.column_settings.sound": "Emetre un son", - "notifications.column_settings.follow": "Nòus abonats :", - "notifications.column_settings.favourite": "Favorits :", - "notifications.column_settings.mention": "Mencions :", - "notifications.column_settings.reblog": "Partatges :", - "notifications.clear": "Levar", - "notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?", - "notifications.settings": "Paramètres de la colomna", - "privacy.public.short": "Public", - "privacy.public.long": "Mostrar dins los fils publics", - "privacy.unlisted.short": "Pas-listat", - "privacy.unlisted.long": "Mostrar pas dins los fils publics", - "privacy.private.short": "Privat", - "privacy.private.long": "Mostrar pas qu'a vòstres abonats", - "privacy.direct.short": "Dirècte", - "privacy.direct.long": "Mostrar pas qu'a las personas mencionadas", - "privacy.change": "Ajustar la confidencialitat del messatge", - "media_gallery.toggle_visible": "Modificar la visibilitat", - "missing_indicator.label": "Pas trobat", - "follow_request.authorize": "Autorizar", - "follow_request.reject": "Regetar", - "home.settings": "Paramètres de la colomna", - "home.column_settings.basic": "Basic", - "home.column_settings.show_reblogs": "Mostrar los partatges", - "home.column_settings.show_replies": "Mostrar las responsas", - "home.column_settings.advanced": "Avançat", - "home.column_settings.filter_regex": "Filtrar amb una expression racionala", - "report.heading": "Nòu senhalament", - "report.placeholder": "Comentaris addicionals", - "report.submit": "Mandat", - "report.target": "Senhalament" -}; - -export default oc; diff --git a/app/assets/javascripts/components/locales/pt-br.jsx b/app/assets/javascripts/components/locales/pt-br.jsx @@ -1,125 +0,0 @@ -const pt_br = { - "account.block": "Bloquear @{name}", - "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", - "account.edit_profile": "Editar perfil", - "account.follow": "Seguir", - "account.followers": "Seguidores", - "account.follows_you": "É teu seguidor", - "account.follows": "Segue", - "account.mention": "Mencionar @{name}", - "account.mute": "Silenciar @{name}", - "account.posts": "Posts", - "account.report": "Denunciar @{name}", - "account.requested": "A aguardar aprovação", - "account.unblock": "Não bloquear @{name}", - "account.unfollow": "Deixar de seguir", - "account.unmute": "Não silenciar @{name}", - "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", - "column_back_button.label": "Voltar", - "column.blocks": "Utilizadores Bloqueados", - "column.community": "Local", - "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores Pendentes", - "column.home": "Home", - "column.mutes": "Utilizadores silenciados", - "column.notifications": "Notificações", - "column.public": "Global", - "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", - "compose_form.publish": "Publicar", - "compose_form.sensitive": "Marcar media como conteúdo sensível", - "compose_form.spoiler_placeholder": "Aviso de conteúdo", - "compose_form.spoiler": "Esconder texto com aviso", - "emoji_button.label": "Inserir Emoji", - "empty_column.community": "Ainda não existem conteúdo local para mostrar!", - "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", - "empty_column.home.public_timeline": "global", - "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", - "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", - "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", - "follow_request.authorize": "Autorizar", - "follow_request.reject": "Rejeitar", - "getting_started.apps": "Existem várias aplicações disponíveis", - "getting_started.heading": "Primeiros passos", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", - "home.column_settings.advanced": "Avançado", - "home.column_settings.basic": "Básico", - "home.column_settings.filter_regex": "Filtrar com uma expressão regular", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "home.settings": "Parâmetros da listagem Home", - "lightbox.close": "Fechar", - "loading_indicator.label": "Carregando...", - "media_gallery.toggle_visible": "Esconder/Mostrar", - "missing_indicator.label": "Não encontrado", - "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Local", - "navigation_bar.edit_profile": "Editar perfil", - "navigation_bar.favourites": "Favoritos", - "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.info": "Mais informações", - "navigation_bar.logout": "Sair", - "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.preferences": "Preferências", - "navigation_bar.public_timeline": "Global", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", - "notification.mention": "{name} mencionou-te", - "notification.reblog": "{name} partilhou o teu post", - "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", - "notifications.clear": "Limpar notificações", - "notifications.column_settings.alert": "Notificações no computador", - "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.follow": "Novos seguidores:", - "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.reblog": "Partilhas:", - "notifications.column_settings.show": "Mostrar nas colunas", - "notifications.column_settings.sound": "Reproduzir som", - "notifications.settings": "Parâmetros da listagem de Notificações", - "privacy.change": "Ajustar a privacidade da mensagem", - "privacy.direct.long": "Apenas para utilizadores mencionados", - "privacy.direct.short": "Directo", - "privacy.private.long": "Apenas para os seguidores", - "privacy.private.short": "Privado", - "privacy.public.long": "Publicar em todos os feeds", - "privacy.public.short": "Público", - "privacy.unlisted.long": "Não publicar nos feeds públicos", - "privacy.unlisted.short": "Não listar", - "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", - "report.placeholder": "Comentários adicionais", - "report.submit": "Enviar", - "report.target": "Denunciar", - "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "search.placeholder": "Pesquisar", - "search.status_by": "Post de {name}", - "status.delete": "Eliminar", - "status.favourite": "Adicionar aos favoritos", - "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", - "status.mention": "Mencionar @{name}", - "status.open": "Expandir", - "status.reblog": "Partilhar", - "status.reblogged_by": "{name} partilhou", - "status.reply": "Responder", - "status.report": "Denúnciar @{name}", - "status.sensitive_toggle": "Clique para ver", - "status.sensitive_warning": "Conteúdo sensível", - "status.show_less": "Mostrar menos", - "status.show_more": "Mostrar mais", - "tabs_bar.compose": "Criar", - "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notificações", - "upload_area.title": "Arraste e solte para enviar", - "upload_button.label": "Adicionar media", - "upload_form.undo": "Anular", - "upload_progress.label": "A gravar...", - "video_player.toggle_sound": "Ligar/Desligar som", - "video_player.toggle_visible": "Ligar/Desligar vídeo", - "video_player.expand": "Expandir vídeo", - "video_player.video_error": "Não é possível ver o vídeo", -}; - -export default pt_br; diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx @@ -1,125 +0,0 @@ -const pt = { - "account.block": "Bloquear @{name}", - "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", - "account.edit_profile": "Editar perfil", - "account.follow": "Seguir", - "account.followers": "Seguidores", - "account.follows_you": "É teu seguidor", - "account.follows": "Segue", - "account.mention": "Mencionar @{name}", - "account.mute": "Silenciar @{name}", - "account.posts": "Posts", - "account.report": "Denunciar @{name}", - "account.requested": "A aguardar aprovação", - "account.unblock": "Não bloquear @{name}", - "account.unfollow": "Deixar de seguir", - "account.unmute": "Não silenciar @{name}", - "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", - "column_back_button.label": "Voltar", - "column.blocks": "Utilizadores Bloqueados", - "column.community": "Local", - "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores Pendentes", - "column.home": "Home", - "column.mutes": "Utilizadores silenciados", - "column.notifications": "Notificações", - "column.public": "Global", - "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", - "compose_form.publish": "Publicar", - "compose_form.sensitive": "Marcar media como conteúdo sensível", - "compose_form.spoiler_placeholder": "Aviso de conteúdo", - "compose_form.spoiler": "Esconder texto com aviso", - "emoji_button.label": "Inserir Emoji", - "empty_column.community": "Ainda não existem conteúdo local para mostrar!", - "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", - "empty_column.home.public_timeline": "global", - "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", - "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", - "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", - "follow_request.authorize": "Autorizar", - "follow_request.reject": "Rejeitar", - "getting_started.apps": "Existem várias aplicações disponíveis", - "getting_started.heading": "Primeiros passos", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", - "home.column_settings.advanced": "Avançado", - "home.column_settings.basic": "Básico", - "home.column_settings.filter_regex": "Filtrar com uma expressão regular", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "home.settings": "Parâmetros da listagem Home", - "lightbox.close": "Fechar", - "loading_indicator.label": "Carregando...", - "media_gallery.toggle_visible": "Esconder/Mostrar", - "missing_indicator.label": "Não encontrado", - "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Local", - "navigation_bar.edit_profile": "Editar perfil", - "navigation_bar.favourites": "Favoritos", - "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.info": "Mais informações", - "navigation_bar.logout": "Sair", - "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.preferences": "Preferências", - "navigation_bar.public_timeline": "Global", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", - "notification.mention": "{name} mencionou-te", - "notification.reblog": "{name} partilhou o teu post", - "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", - "notifications.clear": "Limpar notificações", - "notifications.column_settings.alert": "Notificações no computador", - "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.follow": "Novos seguidores:", - "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.reblog": "Partilhas:", - "notifications.column_settings.show": "Mostrar nas colunas", - "notifications.column_settings.sound": "Reproduzir som", - "notifications.settings": "Parâmetros da listagem de Notificações", - "privacy.change": "Ajustar a privacidade da mensagem", - "privacy.direct.long": "Apenas para utilizadores mencionados", - "privacy.direct.short": "Directo", - "privacy.private.long": "Apenas para os seguidores", - "privacy.private.short": "Privado", - "privacy.public.long": "Publicar em todos os feeds", - "privacy.public.short": "Público", - "privacy.unlisted.long": "Não publicar nos feeds públicos", - "privacy.unlisted.short": "Não listar", - "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", - "report.placeholder": "Comentários adicionais", - "report.submit": "Enviar", - "report.target": "Denunciar", - "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "search.placeholder": "Pesquisar", - "search.status_by": "Post de {name}", - "status.delete": "Eliminar", - "status.favourite": "Adicionar aos favoritos", - "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", - "status.mention": "Mencionar @{name}", - "status.open": "Expandir", - "status.reblog": "Partilhar", - "status.reblogged_by": "{name} partilhou", - "status.reply": "Responder", - "status.report": "Denúnciar @{name}", - "status.sensitive_toggle": "Clique para ver", - "status.sensitive_warning": "Conteúdo sensível", - "status.show_less": "Mostrar menos", - "status.show_more": "Mostrar mais", - "tabs_bar.compose": "Criar", - "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notificações", - "upload_area.title": "Arraste e solte para enviar", - "upload_button.label": "Adicionar media", - "upload_form.undo": "Anular", - "upload_progress.label": "A gravar...", - "video_player.toggle_sound": "Ligar/Desligar som", - "video_player.toggle_visible": "Ligar/Desligar vídeo", - "video_player.expand": "Expandir vídeo", - "video_player.video_error": "Não é possível ver o vídeo", -}; - -export default pt; diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx @@ -1,138 +0,0 @@ -const ru = { - "account.block": "Блокировать", - "account.disclaimer": "Это пользователь с другого узла. Число может быть больше.", - "account.edit_profile": "Изменить профиль", - "account.follow": "Подписаться", - "account.followers": "Подписаны", - "account.follows": "Подписки", - "account.follows_you": "Подписан(а) на Вас", - "account.mention": "Упомянуть", - "account.mute": "Заглушить", - "account.posts": "Посты", - "account.report": "Пожаловаться", - "account.requested": "Ожидает подтверждения", - "account.unblock": "Разблокировать", - "account.unfollow": "Отписаться", - "account.unmute": "Снять глушение", - "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", - "column.blocks": "Список блокировки", - "column.community": "Локальная лента", - "column.favourites": "Понравившееся", - "column.follow_requests": "Запросы на подписку", - "column.home": "Главная", - "column.mutes": "Список глушения", - "column.notifications": "Уведомления", - "column.public": "Глобальная лента", - "column_back_button.label": "Назад", - "column_subheading.navigation": "Навигация", - "column_subheading.settings": "Настройки", - "compose_form.placeholder": "О чем Вы думаете?", - "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.", - "compose_form.publish": "Трубить", - "compose_form.sensitive": "Отметить как чувствительный контент", - "compose_form.spoiler": "Скрыть текст за предупреждением", - "compose_form.spoiler_placeholder": "Предупреждение о скрытом тексте", - "emoji_button.activity": "Занятия", - "emoji_button.flags": "Флаги", - "emoji_button.food": "Еда и напитки", - "emoji_button.label": "Вставить эмодзи", - "emoji_button.nature": "Природа", - "emoji_button.objects": "Предметы", - "emoji_button.people": "Люди", - "emoji_button.search": "Найти...", - "emoji_button.symbols": "Символы", - "emoji_button.travel": "Путешествия", - "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", - "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", - "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", - "empty_column.home.public_timeline": "публичные ленты", - "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", - "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", - "follow_request.authorize": "Авторизовать", - "follow_request.reject": "Отказать", - "getting_started.apps": "Доступны различные приложения.", - "getting_started.heading": "Добро пожаловать", - "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.", - "home.column_settings.advanced": "Дополнительные", - "home.column_settings.basic": "Основные", - "home.column_settings.filter_regex": "Отфильтровать регулярным выражением", - "home.column_settings.show_reblogs": "Показывать продвижения", - "home.column_settings.show_replies": "Показывать ответы", - "home.settings": "Настройки колонки", - "lightbox.close": "Закрыть", - "loading_indicator.label": "Загрузка...", - "media_gallery.toggle_visible": "Показать/скрыть", - "missing_indicator.label": "Не найдено", - "navigation_bar.blocks": "Список блокировки", - "navigation_bar.community_timeline": "Локальная лента", - "navigation_bar.edit_profile": "Изменить профиль", - "navigation_bar.favourites": "Понравившееся", - "navigation_bar.follow_requests": "Запросы на подписку", - "navigation_bar.info": "Об узле", - "navigation_bar.logout": "Выйти", - "navigation_bar.mutes": "Список глушения", - "navigation_bar.preferences": "Опции", - "navigation_bar.public_timeline": "Глобальная лента", - "notification.favourite": "{name} понравился Ваш статус", - "notification.follow": "{name} подписался(-лась) на Вас", - "notification.mention": "{name} упомянул(а) Вас", - "notification.reblog": "{name} продвинул(а) Ваш статус", - "notifications.clear": "Очистить уведомления", - "notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?", - "notifications.column_settings.alert": "Десктопные уведомления", - "notifications.column_settings.favourite": "Нравится:", - "notifications.column_settings.follow": "Новые подписчики:", - "notifications.column_settings.mention": "Упоминания:", - "notifications.column_settings.reblog": "Продвижения:", - "notifications.column_settings.show": "Показывать в колонке", - "notifications.column_settings.sound": "Проигрывать звук", - "notifications.settings": "Настройки колонки", - "privacy.change": "Изменить видимость статуса", - "privacy.direct.long": "Показать только упомянутым", - "privacy.direct.short": "Направленный", - "privacy.private.long": "Показать только подписчикам", - "privacy.private.short": "Приватный", - "privacy.public.long": "Показать в публичных лентах", - "privacy.public.short": "Публичный", - "privacy.unlisted.long": "Не показывать в лентах", - "privacy.unlisted.short": "Скрытый", - "reply_indicator.cancel": "Отмена", - "report.heading": "Новая жалоба", - "report.placeholder": "Комментарий", - "report.submit": "Отправить", - "report.target": "Жалуемся на", - "search.placeholder": "Поиск", - "search.status_by": "Статус от {name}", - "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", - "status.cannot_reblog": "Этот статус не может быть продвинут", - "status.delete": "Удалить", - "status.favourite": "Нравится", - "status.load_more": "Показать еще", - "status.media_hidden": "Медиаконтент скрыт", - "status.mention": "Упомянуть @{name}", - "status.open": "Развернуть статус", - "status.reblog": "Продвинуть", - "status.reblogged_by": "{name} продвинул(а)", - "status.reply": "Ответить", - "status.replyAll": "Ответить на тред", - "status.report": "Пожаловаться", - "status.sensitive_toggle": "Нажмите для просмотра", - "status.sensitive_warning": "Чувствительный контент", - "status.show_less": "Свернуть", - "status.show_more": "Развернуть", - "tabs_bar.compose": "Написать", - "tabs_bar.federated_timeline": "Глобальная", - "tabs_bar.home": "Главная", - "tabs_bar.local_timeline": "Локальная", - "tabs_bar.notifications": "Уведомления", - "upload_area.title": "Перетащите сюда, чтобы загрузить", - "upload_button.label": "Добавить медиаконтент", - "upload_form.undo": "Отменить", - "upload_progress.label": "Загрузка...", - "video_player.expand": "Развернуть видео", - "video_player.toggle_sound": "Вкл./выкл. звук", - "video_player.toggle_visible": "Показать/скрыть", - "video_player.video_error": "Видео не может быть проиграно", -}; - -export default ru; diff --git a/app/assets/javascripts/components/locales/uk.jsx b/app/assets/javascripts/components/locales/uk.jsx @@ -1,57 +0,0 @@ -const uk = { - "column_back_button.label": "Назад", - "lightbox.close": "Закрити", - "loading_indicator.label": "Завантаження...", - "status.mention": "Згадати", - "status.delete": "Видалити", - "status.reply": "Відповісти", - "status.reblog": "Передмухнути", - "status.favourite": "Подобається", - "status.reblogged_by": "{name} передмухнув(-ла)", - "status.sensitive_warning": "Непристойний зміст", - "status.sensitive_toggle": "Натисніть, щоб подивитися", - "video_player.toggle_sound": "Увімкнути/вимкнути звук", - "account.mention": "Згадати", - "account.edit_profile": "Налаштування профілю", - "account.unblock": "Розблокувати", - "account.unfollow": "Відписатися", - "account.block": "Заблокувати", - "account.follow": "Підписатися", - "account.posts": "Пости", - "account.follows": "Підписки", - "account.followers": "Підписники", - "account.follows_you": "Підписаний", - "getting_started.heading": "Ласкаво просимо", - "getting_started.about_addressing": "Ви можете підписуватись на людей, якщо ви знаєте їх ім'я користувача чи домен, шляхом введення email-подібної адреси у верхньому рядку бокової панелі.", - "getting_started.about_shortcuts": "Якщо користувач, якого ви шукаєте, знаходиться на тому ж домені, що й ви, можна просто ввести ім'я користувача. Це правило стосується й згадування людей у статусах.", - "getting_started.about_developer": "Розробник проекту знаходиться за адресою Gargron@mastodon.social", - "column.home": "Головна", - "column.mentions": "Згадування", - "column.public": "Стіна", - "column.notifications": "Сповіщення", - "tabs_bar.compose": "Написати", - "tabs_bar.home": "Головна", - "tabs_bar.mentions": "Згадування", - "tabs_bar.public": "Стіна", - "tabs_bar.notifications": "Сповіщення", - "compose_form.placeholder": "Що у Вас на думці?", - "compose_form.publish": "Дмухнути", - "compose_form.sensitive": "Непристойний зміст", - "compose_form.unlisted": "Таємний режим", - "navigation_bar.edit_profile": "Редагувати профіль", - "navigation_bar.preferences": "Налаштування", - "navigation_bar.public_timeline": "Публічна стіна", - "navigation_bar.logout": "Вийти", - "reply_indicator.cancel": "Відмінити", - "search.placeholder": "Пошук", - "search.account": "Аккаунт", - "search.hashtag": "Хештеґ", - "upload_button.label": "Додати медіа", - "upload_form.undo": "Відмінити", - "notification.follow": "{name} підписався(-лась) на Вас", - "notification.favourite": "{name} сподобався ваш допис", - "notification.reblog": "{name} передмухнув(-ла) Ваш статус", - "notification.mention": "{name} згадав(-ла) Вас" -}; - -export default uk; diff --git a/app/assets/javascripts/components/locales/zh-cn.jsx b/app/assets/javascripts/components/locales/zh-cn.jsx @@ -1,157 +0,0 @@ -import zh from 'react-intl/locale-data/zh'; - -const localeData = zh.reduce(function (acc, localeData) { - if (localeData.locale === "zh-Hans-CN") { - // rename the locale "zh-Hans-CN" as "zh-CN" - // (match the code usually used in Accepted-Language header) - acc.push(Object.assign({}, - localeData, - { - "locale": "zh-CN", - "parentLocale": "zh-Hans-CN", - } - )); - } - return acc; -}, []); - -export { localeData as localeData }; - -const zh_cn = { - "account.block": "屏蔽 @{name}", - "account.disclaimer": "由于这个账户处于另一个服务站,实际数字会比这个更多。", - "account.edit_profile": "修改个人资料", - "account.follow": "关注", - "account.followers": "关注者", - "account.follows_you": "关注你", - "account.follows": "正关注", - "account.mention": "提及 @{name}", - "account.mute": "将 @{name} 静音", - "account.posts": "嘟文", - "account.report": "举报 @{name}", - "account.requested": "等候审批", - "account.unblock": "解除对 @{name} 的屏蔽", - "account.unfollow": "取消关注", - "account.unmute": "取消 @{name} 的静音", - "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", - "column_back_button.label": "返回", - "column.blocks": "屏蔽用户", - "column.community": "本站时间轴", - // intentional departure from existing "推文" translation for posts: - // "推文" refers to "推特", the official translation for Twitter. - // Currently using a semi-phonetic translation "嘟", which refers - // to train horn sounds, for "toot". - "column.favourites": "赞过的嘟文", - "column.follow_requests": "关注请求", - "column.home": "主页", - "column.notifications": "通知", - "column.public": "跨站公共时间轴", - "compose_form.placeholder": "在想啥?", - "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务站} other {之中有些不是 Mastodon 服务站}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。", - "compose_form.private": "标示为“只有关注你的人能看”", - // Going "toot-toot" here below. - "compose_form.publish": "嘟嘟", - "compose_form.sensitive": "将媒体文件标示为“敏感内容”", - "compose_form.spoiler_placeholder": "敏感内容的警告消息", - "compose_form.spoiler": "将部分文本藏于警告消息之后", - "compose_form.unlisted": "请勿在公共时间轴显示", - "emoji_button.label": "加入表情符号", - "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!", - "empty_column.hashtag": "这个标签暂时未有内容。", - "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", - "empty_column.home.public_timeline": "公共时间轴", - "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", - "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", - "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务站的用户吧!你和本站、友站的交流,将决定这里出现的内容。", - "follow_request.authorize": "批准", - "follow_request.reject": "拒绝", - "getting_started.about_addressing": "只要你知道一位用户的用户名称和域名,你可以用“@用户名称@域名”的格式在搜索栏寻找该用户。", - "getting_started.about_shortcuts": "只要该用户是在你现在的服务站开立,你就可以直接输入用户名搜索。在嘟文中提及别的用户也是如此。", - "getting_started.apps": "手机或桌面应用程序", - "getting_started.heading": "开始使用", - "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。你亦可通过{apps}阅读 Mastodon 上的消息。", - "home.column_settings.advanced": "高端", - "home.column_settings.basic": "基本", - "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤", - "home.column_settings.show_reblogs": "显示被转的嘟文", - "home.column_settings.show_replies": "显示回应嘟文", - "home.settings": "字段设置", - "lightbox.close": "关闭", - "loading_indicator.label": "加载中……", - "media_gallery.toggle_visible": "打开或关上", - "missing_indicator.label": "找不到内容", - "navigation_bar.blocks": "被屏蔽的用户", - "navigation_bar.community_timeline": "本站时间轴", - "navigation_bar.edit_profile": "修改个人资料", - "navigation_bar.favourites": "赞的内容", - "navigation_bar.follow_requests": "关注请求", - "navigation_bar.info": "关于本服务站", - "navigation_bar.logout": "注销", - // intentional departure from https://github.com/tootsuite/mastodon/blob/f864fee1/config/locales/zh-CN.yml#L126: - // clashes for settings/preferences - "navigation_bar.preferences": "首选项", - "navigation_bar.public_timeline": "跨站公共时间轴", - "notification.favourite": "{name} 赞你的嘟文", - "notification.follow": "{name} 开始关注你", - "notification.mention": "{name} 提及你", - "notification.reblog": "{name} 转嘟你的嘟文", - "notifications.clear_confirmation": "你确定要清空通知纪录吗?", - "notifications.clear": "清空通知纪录", - "notifications.column_settings.alert": "显示桌面通知", - "notifications.column_settings.favourite": "赞你的嘟文:", - "notifications.column_settings.follow": "关注你:", - "notifications.column_settings.mention": "提及你:", - "notifications.column_settings.reblog": "转你的嘟文:", - "notifications.column_settings.show": "在通知栏显示", - "notifications.column_settings.sound": "播放音效", - "notifications.settings": "字段设置", - "privacy.change": "调整隐私设置", - "privacy.direct.long": "只有提及的用户能看到", - "privacy.direct.short": "私人消息", - "privacy.private.long": "只有关注你用户能看到", - "privacy.private.short": "关注者", - "privacy.public.long": "在公共时间轴显示", - "privacy.public.short": "公共", - "privacy.unlisted.long": "公开,但不在公共时间轴显示", - "privacy.unlisted.short": "公开", - "reply_indicator.cancel": "取消", - "report.heading": "举报", - "report.placeholder": "额外消息", - "report.submit": "提交", - "report.target": "Reporting", - "search_results.total": "{count, number} 项结果", - "search.account": "用户", - "search.hashtag": "标签", - "search.placeholder": "搜索", - "search.status_by": "按{name}搜索嘟文", - "status.delete": "删除", - "status.favourite": "赞", - "status.load_more": "加载更多", - "status.media_hidden": "隐藏媒体内容", - "status.mention": "提及 @{name}", - "status.open": "展开嘟文", - "status.reblog": "转嘟", - "status.reblogged_by": "{name} 转嘟", - "status.reply": "回应", - "status.report": "举报 @{name}", - "status.sensitive_toggle": "点击显示", - "status.sensitive_warning": "敏感内容", - "status.show_less": "减少显示", - "status.show_more": "显示更多", - "tabs_bar.compose": "撰写", - "tabs_bar.federated_timeline": "跨站", - "tabs_bar.home": "主页", - "tabs_bar.local_timeline": "本站", - "tabs_bar.mentions": "提及", - "tabs_bar.notifications": "通知", - "tabs_bar.public": "跨站公共时间轴", - "upload_area.title": "将文件拖放至此上传", - "upload_button.label": "上传媒体文件", - "upload_form.undo": "还原", - "upload_progress.label": "上传中……", - "video_player.expand": "展开影片", - "video_player.toggle_sound": "开关音效", - "video_player.toggle_visible": "打开或关上", -}; - -export default zh_cn; diff --git a/app/assets/javascripts/components/locales/zh-hk.jsx b/app/assets/javascripts/components/locales/zh-hk.jsx @@ -1,150 +0,0 @@ -import zh from 'react-intl/locale-data/zh'; - -const localeData = zh.reduce(function (acc, localeData) { - if (localeData.locale === "zh-Hant-HK") { - // rename the locale "zh-Hant-HK" as "zh-HK" - // (match the code usually used in Accepted-Language header) - acc.push(Object.assign({}, - localeData, - { - "locale": "zh-HK", - "parentLocale": "zh-Hant-HK", - } - )); - } - return acc; -}, []); - -export { localeData as localeData }; - -const zh_hk = { - "account.block": "封鎖 @{name}", - "account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。", - "account.edit_profile": "修改個人資料", - "account.follow": "關注", - "account.followers": "關注的人", - "account.follows_you": "關注你", - "account.follows": "正在關注", - "account.mention": "提及 @{name}", - "account.mute": "將 @{name} 靜音", - "account.posts": "文章", - "account.report": "舉報 @{name}", - "account.requested": "等候審批", - "account.unblock": "解除對 @{name} 的封鎖", - "account.unfollow": "取消關注", - "account.unmute": "取消 @{name} 的靜音", - "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", - "column_back_button.label": "返回", - "column.blocks": "封鎖用戶", - "column.community": "本站時間軸", - "column.favourites": "喜歡的文章", - "column.follow_requests": "關注請求", - "column.home": "主頁", - "column.notifications": "通知", - "column.public": "跨站公共時間軸", - "compose_form.placeholder": "你在想甚麼?", - "compose_form.privacy_disclaimer": "你的私人文章,將被遞送至你所提及的 {domains} 用戶。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於各 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將無法收到這篇文章的私隱設定,然後可能被轉推給不能預知的用戶閱讀。", - "compose_form.private": "標示為「只有關注你的人能看」", - "compose_form.publish": "發文", - "compose_form.sensitive": "將媒體檔案標示為「敏感內容」", - "compose_form.spoiler_placeholder": "敏感警告訊息", - "compose_form.spoiler": "將部份文字藏於警告訊息之後", - "compose_form.unlisted": "請勿在公共時間軸顯示", - "emoji_button.label": "加入表情符號", - "empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!", - "empty_column.hashtag": "這個標籤暫時未有內容。", - "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", - "empty_column.home.public_timeline": "公共時間軸", - "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", - "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。", - "empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。", - "follow_request.authorize": "批准", - "follow_request.reject": "拒絕", - "getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。", - "getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。", - "getting_started.apps": "手機或桌面應用程式", - "getting_started.heading": "開始使用", - "getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。", - "home.column_settings.advanced": "進階", - "home.column_settings.basic": "基本", - "home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾", - "home.column_settings.show_reblogs": "顯示被轉推的文章", - "home.column_settings.show_replies": "顯示回應文章", - "home.settings": "欄位設定", - "lightbox.close": "Close", - "loading_indicator.label": "載入中...", - "media_gallery.toggle_visible": "打開或關上", - "missing_indicator.label": "找不到內容", - "navigation_bar.blocks": "被封鎖的用戶", - "navigation_bar.community_timeline": "本站時間軸", - "navigation_bar.edit_profile": "修改個人資料", - "navigation_bar.favourites": "喜歡的內容", - "navigation_bar.follow_requests": "關注請求", - "navigation_bar.info": "關於本服務站", - "navigation_bar.logout": "登出", - "navigation_bar.preferences": "偏好設定", - "navigation_bar.public_timeline": "跨站公共時間軸", - "notification.favourite": "{name} 喜歡你的文章", - "notification.follow": "{name} 開始關注你", - "notification.mention": "{name} 提及你", - "notification.reblog": "{name} 轉推你的文章", - "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?", - "notifications.clear": "清空通知紀錄", - "notifications.column_settings.alert": "顯示桌面通知", - "notifications.column_settings.favourite": "喜歡你的文章:", - "notifications.column_settings.follow": "關注你:", - "notifications.column_settings.mention": "提及你:", - "notifications.column_settings.reblog": "轉推你的文章:", - "notifications.column_settings.show": "在通知欄顯示", - "notifications.column_settings.sound": "播放音效", - "notifications.settings": "欄位設定", - "privacy.change": "調整私隱設定", - "privacy.direct.long": "只有提及的用戶能看到", - "privacy.direct.short": "私人訊息", - "privacy.private.long": "只有關注你用戶能看到", - "privacy.private.short": "關注者", - "privacy.public.long": "在公共時間軸顯示", - "privacy.public.short": "公共", - "privacy.unlisted.long": "公開,但不在公共時間軸顯示", - "privacy.unlisted.short": "公開", - "reply_indicator.cancel": "取消", - "report.heading": "舉報", - "report.placeholder": "額外訊息", - "report.submit": "提交", - "report.target": "Reporting", - "search_results.total": "{count, number} 項結果", - "search.account": "用戶", - "search.hashtag": "標籤", - "search.placeholder": "搜尋", - "search.status_by": "按{name}搜尋文章", - "status.delete": "刪除", - "status.favourite": "喜歡", - "status.load_more": "載入更多", - "status.media_hidden": "隱藏媒體內容", - "status.mention": "提及 @{name}", - "status.open": "展開文章", - "status.reblog": "轉推", - "status.reblogged_by": "{name} 轉推", - "status.reply": "回應", - "status.report": "舉報 @{name}", - "status.sensitive_toggle": "點擊顯示", - "status.sensitive_warning": "敏感內容", - "status.show_less": "減少顯示", - "status.show_more": "顯示更多", - "tabs_bar.compose": "撰寫", - "tabs_bar.federated_timeline": "跨站", - "tabs_bar.home": "主頁", - "tabs_bar.local_timeline": "本站", - "tabs_bar.mentions": "提及", - "tabs_bar.notifications": "通知", - "tabs_bar.public": "跨站公共時間軸", - "upload_area.title": "將檔案拖放至此上載", - "upload_button.label": "上載媒體檔案", - "upload_form.undo": "還原", - "upload_progress.label": "上載中……", - "video_player.expand": "展開影片", - "video_player.toggle_sound": "開關音效", - "video_player.toggle_visible": "打開或關上", -}; - -export default zh_hk; diff --git a/app/assets/javascripts/components/middleware/errors.jsx b/app/assets/javascripts/components/middleware/errors.jsx @@ -1,33 +0,0 @@ -import { showAlert } from '../actions/alerts'; - -const defaultSuccessSuffix = 'SUCCESS'; -const defaultFailSuffix = 'FAIL'; - -export default function errorsMiddleware() { - return ({ dispatch }) => next => action => { - if (action.type && !action.skipAlert) { - const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); - const isSuccess = new RegExp(`${defaultSuccessSuffix}$`, 'g'); - - if (action.type.match(isFail)) { - if (action.error.response) { - const { data, status, statusText } = action.error.response; - - let message = statusText; - let title = `${status}`; - - if (data.error) { - message = data.error; - } - - dispatch(showAlert(title, message)); - } else { - console.error(action.error); // eslint-disable-line no-console - dispatch(showAlert('Oops!', 'An unexpected error occurred.')); - } - } - } - - return next(action); - }; -}; diff --git a/app/assets/javascripts/components/middleware/loading_bar.jsx b/app/assets/javascripts/components/middleware/loading_bar.jsx @@ -1,25 +0,0 @@ -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/middleware/sounds.jsx b/app/assets/javascripts/components/middleware/sounds.jsx @@ -1,22 +0,0 @@ -const play = audio => { - if (!audio.paused) { - audio.pause(); - audio.fastSeek(0); - } - - audio.play(); -}; - -export default function soundsMiddleware() { - const soundCache = { - boop: new Audio(['/sounds/boop.mp3']) - }; - - return ({ dispatch }) => next => (action) => { - if (action.meta && action.meta.sound && soundCache[action.meta.sound]) { - play(soundCache[action.meta.sound]); - } - - return next(action); - }; -}; diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx @@ -1,131 +0,0 @@ -import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS -} from '../actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS -} from '../actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS -} from '../actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS -} from '../actions/interactions'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS -} from '../actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS -} from '../actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import { - NOTIFICATIONS_UPDATE, - 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'; - -const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account)); - -const normalizeAccounts = (state, accounts) => { - accounts.forEach(account => { - state = normalizeAccount(state, account); - }); - - return state; -}; - -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - -const initialState = Immutable.Map(); - -export default function accounts(state = initialState, action) { - switch(action.type) { - 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 FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - return normalizeAccounts(state, action.accounts); - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_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: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_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); - case ACCOUNT_FOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); - case ACCOUNT_UNFOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/alerts.jsx b/app/assets/javascripts/components/reducers/alerts.jsx @@ -1,25 +0,0 @@ -import { - ALERT_SHOW, - ALERT_DISMISS, - ALERT_CLEAR -} from '../actions/alerts'; -import Immutable from 'immutable'; - -const initialState = Immutable.List([]); - -export default function alerts(state = initialState, action) { - switch(action.type) { - case ALERT_SHOW: - return state.push(Immutable.Map({ - key: state.size > 0 ? state.last().get('key') + 1 : 0, - title: action.title, - message: action.message - })); - case ALERT_DISMISS: - return state.filterNot(item => item.get('key') === action.alert.key); - case ALERT_CLEAR: - return state.clear(); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/cards.jsx b/app/assets/javascripts/components/reducers/cards.jsx @@ -1,14 +0,0 @@ -import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; - -import Immutable from 'immutable'; - -const initialState = Immutable.Map(); - -export default function cards(state = initialState, action) { - switch(action.type) { - case STATUS_CARD_FETCH_SUCCESS: - return state.set(action.id, Immutable.fromJS(action.card)); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx @@ -1,232 +0,0 @@ -import { - COMPOSE_MOUNT, - COMPOSE_UNMOUNT, - COMPOSE_CHANGE, - COMPOSE_REPLY, - COMPOSE_REPLY_CANCEL, - COMPOSE_MENTION, - COMPOSE_SUBMIT_REQUEST, - COMPOSE_SUBMIT_SUCCESS, - COMPOSE_SUBMIT_FAIL, - COMPOSE_UPLOAD_REQUEST, - COMPOSE_UPLOAD_SUCCESS, - COMPOSE_UPLOAD_FAIL, - COMPOSE_UPLOAD_UNDO, - COMPOSE_UPLOAD_PROGRESS, - COMPOSE_SUGGESTIONS_CLEAR, - COMPOSE_SUGGESTIONS_READY, - COMPOSE_SUGGESTION_SELECT, - COMPOSE_SENSITIVITY_CHANGE, - COMPOSE_SPOILERNESS_CHANGE, - COMPOSE_SPOILER_TEXT_CHANGE, - COMPOSE_VISIBILITY_CHANGE, - COMPOSE_LISTABILITY_CHANGE, - COMPOSE_EMOJI_INSERT -} from '../actions/compose'; -import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; -import uuid from '../uuid'; - -const initialState = Immutable.Map({ - mounted: false, - sensitive: false, - spoiler: false, - spoiler_text: '', - privacy: null, - text: '', - focusDate: null, - preselectDate: null, - in_reply_to: null, - is_submitting: false, - is_uploading: false, - progress: 0, - media_attachments: Immutable.List(), - suggestion_token: null, - suggestions: Immutable.List(), - me: null, - default_privacy: 'public', - resetFileKey: Math.floor((Math.random() * 0x10000)), - idempotencyKey: null -}); - -function statusToTextMentions(state, status) { - let set = Immutable.OrderedSet([]); - let me = state.get('me'); - - if (status.getIn(['account', 'id']) !== me) { - set = set.add(`@${status.getIn(['account', 'acct'])} `); - } - - return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); -}; - -function clearAll(state) { - return state.withMutations(map => { - map.set('text', ''); - map.set('spoiler', false); - map.set('spoiler_text', ''); - map.set('is_submitting', false); - map.set('in_reply_to', null); - map.set('privacy', state.get('default_privacy')); - map.set('sensitive', false); - map.update('media_attachments', list => list.clear()); - map.set('idempotencyKey', uuid()); - }); -}; - -function appendMedia(state, media) { - return state.withMutations(map => { - map.update('media_attachments', list => list.push(media)); - map.set('is_uploading', false); - map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - }); -}; - -function removeMedia(state, mediaId) { - const media = state.get('media_attachments').find(item => item.get('id') === mediaId); - const prevSize = state.get('media_attachments').size; - - return state.withMutations(map => { - map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId)); - map.update('text', text => text.replace(media.get('text_url'), '').trim()); - map.set('idempotencyKey', uuid()); - - if (prevSize === 1) { - map.set('sensitive', false); - } - }); -}; - -const insertSuggestion = (state, position, token, completion) => { - return state.withMutations(map => { - map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); - map.set('suggestion_token', null); - map.update('suggestions', Immutable.List(), list => list.clear()); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - }); -}; - -const insertEmoji = (state, position, emojiData) => { - const emoji = emojiData.shortname; - - return state.withMutations(map => { - map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - }); -}; - -const privacyPreference = (a, b) => { - if (a === 'direct' || b === 'direct') { - return 'direct'; - } else if (a === 'private' || b === 'private') { - return 'private'; - } else if (a === 'unlisted' || b === 'unlisted') { - return 'unlisted'; - } else { - return 'public'; - } -}; - -export default function compose(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return clearAll(state.merge(action.state.get('compose'))); - case COMPOSE_MOUNT: - return state.set('mounted', true); - case COMPOSE_UNMOUNT: - return state.set('mounted', false); - case COMPOSE_SENSITIVITY_CHANGE: - return state - .set('sensitive', !state.get('sensitive')) - .set('idempotencyKey', uuid()); - case COMPOSE_SPOILERNESS_CHANGE: - return state.withMutations(map => { - map.set('spoiler_text', ''); - map.set('spoiler', !state.get('spoiler')); - map.set('idempotencyKey', uuid()); - }); - case COMPOSE_SPOILER_TEXT_CHANGE: - return state - .set('spoiler_text', action.text) - .set('idempotencyKey', uuid()); - case COMPOSE_VISIBILITY_CHANGE: - return state - .set('privacy', action.value) - .set('idempotencyKey', uuid()); - case COMPOSE_CHANGE: - return state - .set('text', action.text) - .set('idempotencyKey', uuid()); - case COMPOSE_REPLY: - return state.withMutations(map => { - map.set('in_reply_to', action.status.get('id')); - map.set('text', statusToTextMentions(state, action.status)); - map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); - map.set('focusDate', new Date()); - map.set('preselectDate', new Date()); - map.set('idempotencyKey', uuid()); - - if (action.status.get('spoiler_text').length > 0) { - map.set('spoiler', true); - map.set('spoiler_text', action.status.get('spoiler_text')); - } else { - map.set('spoiler', false); - map.set('spoiler_text', ''); - } - }); - case COMPOSE_REPLY_CANCEL: - return state.withMutations(map => { - map.set('in_reply_to', null); - map.set('text', ''); - map.set('spoiler', false); - map.set('spoiler_text', ''); - map.set('privacy', state.get('default_privacy')); - map.set('idempotencyKey', uuid()); - }); - case COMPOSE_SUBMIT_REQUEST: - return state.set('is_submitting', true); - case COMPOSE_SUBMIT_SUCCESS: - return clearAll(state); - case COMPOSE_SUBMIT_FAIL: - return state.set('is_submitting', false); - case COMPOSE_UPLOAD_REQUEST: - return state.withMutations(map => { - map.set('is_uploading', true); - }); - case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, Immutable.fromJS(action.media)); - case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false); - case COMPOSE_UPLOAD_UNDO: - return removeMedia(state, action.media_id); - case COMPOSE_UPLOAD_PROGRESS: - return state.set('progress', Math.round((action.loaded / action.total) * 100)); - case COMPOSE_MENTION: - return state - .update('text', text => `${text}@${action.account.get('acct')} `) - .set('focusDate', new Date()) - .set('idempotencyKey', uuid()); - case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); - case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); - case COMPOSE_SUGGESTION_SELECT: - return insertSuggestion(state, action.position, action.token, action.completion); - case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { - return state.set('in_reply_to', null); - } else { - return state; - } - case COMPOSE_EMOJI_INSERT: - return insertEmoji(state, action.position, action.emoji); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx @@ -1,36 +0,0 @@ -import { combineReducers } from 'redux-immutable'; -import timelines from './timelines'; -import meta from './meta'; -import compose from './compose'; -import alerts from './alerts'; -import { loadingBarReducer } from 'react-redux-loading-bar'; -import modal from './modal'; -import user_lists from './user_lists'; -import accounts from './accounts'; -import statuses from './statuses'; -import relationships from './relationships'; -import search from './search'; -import notifications from './notifications'; -import settings from './settings'; -import status_lists from './status_lists'; -import cards from './cards'; -import reports from './reports'; - -export default combineReducers({ - timelines, - meta, - compose, - alerts, - loadingBar: loadingBarReducer, - modal, - user_lists, - status_lists, - accounts, - statuses, - relationships, - search, - notifications, - settings, - cards, - reports -}); diff --git a/app/assets/javascripts/components/reducers/meta.jsx b/app/assets/javascripts/components/reducers/meta.jsx @@ -1,17 +0,0 @@ -import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - streaming_api_base_url: null, - access_token: null, - me: null -}); - -export default function meta(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('meta')); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx @@ -1,18 +0,0 @@ -import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; -import Immutable from 'immutable'; - -const initialState = { - modalType: null, - modalProps: {} -}; - -export default function modal(state = initialState, action) { - switch(action.type) { - case MODAL_OPEN: - return { modalType: action.modalType, modalProps: action.modalProps }; - case MODAL_CLOSE: - return initialState; - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx @@ -1,104 +0,0 @@ -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_REFRESH_REQUEST, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_REFRESH_FAIL, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_CLEAR, - NOTIFICATIONS_SCROLL_TOP -} from '../actions/notifications'; -import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - items: Immutable.List(), - next: null, - top: true, - unread: 0, - loaded: false, - isLoading: true -}); - -const notificationToMap = notification => Immutable.Map({ - id: notification.id, - type: notification.type, - account: notification.account.id, - status: notification.status ? notification.status.id : null -}); - -const normalizeNotification = (state, notification) => { - if (!state.get('top')) { - state = state.update('unread', unread => unread + 1); - } - - return state.update('items', list => list.unshift(notificationToMap(notification))); -}; - -const normalizeNotifications = (state, notifications, next) => { - let items = Immutable.List(); - const loaded = state.get('loaded'); - - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(n)); - }); - - if (state.get('next') === null) { - state = state.set('next', next); - } - - return state - .update('items', list => loaded ? list.unshift(...items) : list.push(...items)) - .set('loaded', true) - .set('isLoading', false); -}; - -const appendNormalizedNotifications = (state, notifications, next) => { - let items = Immutable.List(); - - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(n)); - }); - - return state - .update('items', list => list.push(...items)) - .set('next', next) - .set('isLoading', false); -}; - -const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item.get('account') === relationship.id)); -}; - -const updateTop = (state, top) => { - if (top) { - state = state.set('unread', 0); - } - - return state.set('top', top); -}; - -export default function notifications(state = initialState, action) { - switch(action.type) { - case NOTIFICATIONS_REFRESH_REQUEST: - case NOTIFICATIONS_EXPAND_REQUEST: - case NOTIFICATIONS_REFRESH_FAIL: - case NOTIFICATIONS_EXPAND_FAIL: - return state.set('isLoading', true); - case NOTIFICATIONS_SCROLL_TOP: - return updateTop(state, action.top); - case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); - case NOTIFICATIONS_REFRESH_SUCCESS: - return normalizeNotifications(state, action.notifications, action.next); - case NOTIFICATIONS_EXPAND_SUCCESS: - return appendNormalizedNotifications(state, action.notifications, action.next); - case ACCOUNT_BLOCK_SUCCESS: - return filterNotifications(state, action.relationship); - case NOTIFICATIONS_CLEAR: - return state.set('items', Immutable.List()).set('next', null); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/relationships.jsx b/app/assets/javascripts/components/reducers/relationships.jsx @@ -1,38 +0,0 @@ -import { - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_UNBLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNMUTE_SUCCESS, - RELATIONSHIPS_FETCH_SUCCESS -} from '../actions/accounts'; -import Immutable from 'immutable'; - -const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship)); - -const normalizeRelationships = (state, relationships) => { - relationships.forEach(relationship => { - state = normalizeRelationship(state, relationship); - }); - - return state; -}; - -const initialState = Immutable.Map(); - -export default function relationships(state = initialState, action) { - switch(action.type) { - case ACCOUNT_FOLLOW_SUCCESS: - case ACCOUNT_UNFOLLOW_SUCCESS: - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_UNBLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - case ACCOUNT_UNMUTE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/reports.jsx b/app/assets/javascripts/components/reducers/reports.jsx @@ -1,60 +0,0 @@ -import { - REPORT_INIT, - REPORT_SUBMIT_REQUEST, - REPORT_SUBMIT_SUCCESS, - REPORT_SUBMIT_FAIL, - REPORT_CANCEL, - REPORT_STATUS_TOGGLE, - REPORT_COMMENT_CHANGE -} from '../actions/reports'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - new: Immutable.Map({ - isSubmitting: false, - account_id: null, - status_ids: Immutable.Set(), - comment: '' - }) -}); - -export default function reports(state = initialState, action) { - switch(action.type) { - case REPORT_INIT: - return state.withMutations(map => { - map.setIn(['new', 'isSubmitting'], false); - map.setIn(['new', 'account_id'], action.account.get('id')); - - if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { - map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set()); - map.setIn(['new', 'comment'], ''); - } else { - map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); - } - }); - case REPORT_STATUS_TOGGLE: - return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => { - if (action.checked) { - return set.add(action.statusId); - } - - return set.remove(action.statusId); - }); - case REPORT_COMMENT_CHANGE: - return state.setIn(['new', 'comment'], action.comment); - case REPORT_SUBMIT_REQUEST: - return state.setIn(['new', 'isSubmitting'], true); - case REPORT_SUBMIT_FAIL: - return state.setIn(['new', 'isSubmitting'], false); - case REPORT_CANCEL: - case REPORT_SUBMIT_SUCCESS: - return state.withMutations(map => { - map.setIn(['new', 'account_id'], null); - map.setIn(['new', 'status_ids'], Immutable.Set()); - map.setIn(['new', 'comment'], ''); - map.setIn(['new', 'isSubmitting'], false); - }); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx @@ -1,96 +0,0 @@ -import { - SEARCH_CHANGE, - SEARCH_CLEAR, - SEARCH_FETCH_SUCCESS, - SEARCH_SHOW -} from '../actions/search'; -import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - value: '', - submitted: false, - hidden: false, - results: Immutable.Map() -}); - -const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => { - let newSuggestions = []; - - if (accounts.length > 0) { - newSuggestions.push({ - title: 'account', - items: accounts.map(item => ({ - type: 'account', - id: item.id, - value: item.acct - })) - }); - } - - if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 || hashtags.length > 0) { - let hashtagItems = hashtags.map(item => ({ - type: 'hashtag', - id: item, - value: `#${item}` - })); - - if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 && !value.startsWith('http://') && !value.startsWith('https://') && hashtags.indexOf(value) === -1) { - hashtagItems.unshift({ - type: 'hashtag', - id: value, - value: `#${value}` - }); - } - - if (hashtagItems.length > 0) { - newSuggestions.push({ - title: 'hashtag', - items: hashtagItems - }); - } - } - - if (statuses.length > 0) { - newSuggestions.push({ - title: 'status', - items: statuses.map(item => ({ - type: 'status', - id: item.id, - value: item.id - })) - }); - } - - return state.withMutations(map => { - map.set('suggestions', newSuggestions); - map.set('loaded_value', value); - }); -}; - -export default function search(state = initialState, action) { - switch(action.type) { - case SEARCH_CHANGE: - return state.set('value', action.value); - case SEARCH_CLEAR: - return state.withMutations(map => { - map.set('value', ''); - map.set('results', Immutable.Map()); - map.set('submitted', false); - map.set('hidden', false); - }); - case SEARCH_SHOW: - return state.set('hidden', false); - case COMPOSE_REPLY: - case COMPOSE_MENTION: - return state.set('hidden', true); - case SEARCH_FETCH_SUCCESS: - return state.set('results', Immutable.Map({ - accounts: Immutable.List(action.results.accounts.map(item => item.id)), - statuses: Immutable.List(action.results.statuses.map(item => item.id)), - hashtags: Immutable.List(action.results.hashtags) - })).set('submitted', true); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx @@ -1,48 +0,0 @@ -import { SETTING_CHANGE } from '../actions/settings'; -import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - onboarded: false, - - home: Immutable.Map({ - shows: Immutable.Map({ - reblog: true, - reply: true - }) - }), - - notifications: Immutable.Map({ - alerts: Immutable.Map({ - follow: true, - favourite: true, - reblog: true, - mention: true - }), - - shows: Immutable.Map({ - follow: true, - favourite: true, - reblog: true, - mention: true - }), - - sounds: Immutable.Map({ - follow: true, - favourite: true, - reblog: true, - mention: true - }) - }) -}); - -export default function settings(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.mergeDeep(action.state.get('settings')); - case SETTING_CHANGE: - return state.setIn(action.key, action.value); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/status_lists.jsx b/app/assets/javascripts/components/reducers/status_lists.jsx @@ -1,39 +0,0 @@ -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 @@ -1,124 +0,0 @@ -import { - REBLOG_REQUEST, - REBLOG_SUCCESS, - REBLOG_FAIL, - UNREBLOG_SUCCESS, - FAVOURITE_REQUEST, - FAVOURITE_SUCCESS, - FAVOURITE_FAIL, - UNFAVOURITE_SUCCESS -} from '../actions/interactions'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS -} from '../actions/statuses'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS -} from '../actions/timelines'; -import { - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, - ACCOUNT_BLOCK_SUCCESS -} from '../actions/accounts'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS -} from '../actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS -} from '../actions/favourites'; -import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import Immutable from 'immutable'; - -const normalizeStatus = (state, status) => { - if (!status) { - return state; - } - - const normalStatus = { ...status }; - normalStatus.account = status.account.id; - - if (status.reblog && status.reblog.id) { - state = normalizeStatus(state, status.reblog); - normalStatus.reblog = status.reblog.id; - } - - const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); - normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent; - - return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus))); -}; - -const normalizeStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeStatus(state, status); - }); - - return state; -}; - -const deleteStatus = (state, id, references) => { - references.forEach(ref => { - state = deleteStatus(state, ref[0], []); - }); - - return state.delete(id); -}; - -const filterStatuses = (state, relationship) => { - state.forEach(status => { - if (status.get('account') !== relationship.id) { - return; - } - - state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id'))); - }); - - return state; -}; - -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: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case SEARCH_FETCH_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/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx @@ -1,317 +0,0 @@ -import { - TIMELINE_REFRESH_REQUEST, - TIMELINE_REFRESH_SUCCESS, - TIMELINE_REFRESH_FAIL, - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_FAIL, - TIMELINE_SCROLL_TOP, - TIMELINE_CONNECT, - TIMELINE_DISCONNECT -} from '../actions/timelines'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS -} from '../actions/interactions'; -import { - ACCOUNT_TIMELINE_FETCH_REQUEST, - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_FETCH_FAIL, - ACCOUNT_TIMELINE_EXPAND_REQUEST, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_FAIL, - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS -} from '../actions/accounts'; -import { - CONTEXT_FETCH_SUCCESS -} from '../actions/statuses'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - home: Immutable.Map({ - path: () => '/api/v1/timelines/home', - next: null, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List() - }), - - public: Immutable.Map({ - path: () => '/api/v1/timelines/public', - next: null, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List() - }), - - community: Immutable.Map({ - path: () => '/api/v1/timelines/public', - next: null, - params: { local: true }, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List() - }), - - tag: Immutable.Map({ - path: (id) => `/api/v1/timelines/tag/${id}`, - next: null, - isLoading: false, - id: null, - loaded: false, - top: true, - unread: 0, - items: Immutable.List() - }), - - accounts_timelines: Immutable.Map(), - ancestors: Immutable.Map(), - descendants: Immutable.Map() -}); - -const normalizeStatus = (state, status) => { - const replyToId = status.get('in_reply_to_id'); - const id = status.get('id'); - - if (replyToId) { - if (!state.getIn(['descendants', replyToId], Immutable.List()).includes(id)) { - state = state.updateIn(['descendants', replyToId], Immutable.List(), set => set.push(id)); - } - - if (!state.getIn(['ancestors', id], Immutable.List()).includes(replyToId)) { - state = state.updateIn(['ancestors', id], Immutable.List(), set => set.push(replyToId)); - } - } - - return state; -}; - -const normalizeTimeline = (state, timeline, statuses, next) => { - let ids = Immutable.List(); - const loaded = state.getIn([timeline, 'loaded']); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - ids = ids.set(i, status.get('id')); - }); - - state = state.setIn([timeline, 'loaded'], true); - state = state.setIn([timeline, 'isLoading'], false); - - if (state.getIn([timeline, 'next']) === null) { - state = state.setIn([timeline, 'next'], next); - } - - return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : ids)); -}; - -const appendNormalizedTimeline = (state, timeline, statuses, next) => { - let moreIds = Immutable.List(); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - moreIds = moreIds.set(i, status.get('id')); - }); - - state = state.setIn([timeline, 'isLoading'], false); - state = state.setIn([timeline, 'next'], next); - - return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds)); -}; - -const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => { - let ids = Immutable.List(); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - ids = ids.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .set('loaded', true) - .set('next', true) - .update('items', Immutable.List(), list => (replace ? ids : list.unshift(...ids)))); -}; - -const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { - let moreIds = Immutable.List([]); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - moreIds = moreIds.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .set('next', next) - .update('items', list => list.push(...moreIds))); -}; - -const updateTimeline = (state, timeline, status, references) => { - const top = state.getIn([timeline, 'top']); - - state = normalizeStatus(state, status); - - if (!top) { - state = state.updateIn([timeline, 'unread'], unread => unread + 1); - } - - state = state.updateIn([timeline, 'items'], Immutable.List(), list => { - if (top && list.size > 40) { - list = list.take(20); - } - - if (list.includes(status.get('id'))) { - return list; - } - - const reblogOfId = status.getIn(['reblog', 'id'], null); - - if (reblogOfId !== null) { - list = list.filterNot(itemId => references.includes(itemId)); - } - - return list.unshift(status.get('id')); - }); - - return state; -}; - -const deleteStatus = (state, id, accountId, references, reblogOf) => { - if (reblogOf) { - // If we are deleting a reblog, just replace reblog with its original - return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item)); - } - - // Remove references from timelines - ['home', 'public', 'community', 'tag'].forEach(function (timeline) { - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); - }); - - // Remove references from account timelines - state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); - - // Remove references from context - state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { - state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); - }); - - state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { - state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); - }); - - state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); - - // Remove reblogs of deleted status - references.forEach(ref => { - state = deleteStatus(state, ref[0], ref[1], []); - }); - - return state; -}; - -const filterTimelines = (state, relationship, statuses) => { - let references; - - statuses.forEach(status => { - if (status.get('account') !== relationship.id) { - return; - } - - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references); - }); - - return state; -}; - -const normalizeContext = (state, id, ancestors, descendants) => { - const ancestorsIds = ancestors.map(ancestor => ancestor.get('id')); - const descendantsIds = descendants.map(descendant => descendant.get('id')); - - return state.withMutations(map => { - map.setIn(['ancestors', id], ancestorsIds); - map.setIn(['descendants', id], descendantsIds); - }); -}; - -const resetTimeline = (state, timeline, id) => { - if (timeline === 'tag' && typeof id !== 'undefined' && state.getIn([timeline, 'id']) !== id) { - state = state.update(timeline, map => map - .set('id', id) - .set('isLoading', true) - .set('loaded', false) - .set('next', null) - .set('top', true) - .update('items', list => list.clear())); - } else { - state = state.setIn([timeline, 'isLoading'], true); - } - - return state; -}; - -const updateTop = (state, timeline, top) => { - if (top) { - state = state.setIn([timeline, 'unread'], 0); - } - - return state.setIn([timeline, 'top'], top); -}; - -export default function timelines(state = initialState, action) { - switch(action.type) { - case TIMELINE_REFRESH_REQUEST: - case TIMELINE_EXPAND_REQUEST: - return resetTimeline(state, action.timeline, action.id); - case TIMELINE_REFRESH_FAIL: - case TIMELINE_EXPAND_FAIL: - return state.setIn([action.timeline, 'isLoading'], false); - case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); - case TIMELINE_EXPAND_SUCCESS: - return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); - case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); - case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); - case ACCOUNT_TIMELINE_FETCH_REQUEST: - case ACCOUNT_TIMELINE_EXPAND_REQUEST: - return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); - case ACCOUNT_TIMELINE_FETCH_FAIL: - case ACCOUNT_TIMELINE_EXPAND_FAIL: - return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: - return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterTimelines(state, action.relationship, action.statuses); - case TIMELINE_SCROLL_TOP: - return updateTop(state, action.timeline, action.top); - case TIMELINE_CONNECT: - return state.setIn([action.timeline, 'online'], true); - case TIMELINE_DISCONNECT: - return state.setIn([action.timeline, 'online'], false); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx @@ -1,80 +0,0 @@ -import { - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_REJECT_SUCCESS -} from '../actions/accounts'; -import { - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS -} from '../actions/interactions'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS -} from '../actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS -} from '../actions/mutes'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - followers: Immutable.Map(), - following: Immutable.Map(), - reblogged_by: Immutable.Map(), - favourited_by: Immutable.Map(), - follow_requests: Immutable.Map(), - blocks: Immutable.Map(), - mutes: Immutable.Map() -}); - -const normalizeList = (state, type, id, accounts, next) => { - return state.setIn([type, id], Immutable.Map({ - next, - items: Immutable.List(accounts.map(item => item.id)) - })); -}; - -const appendToList = (state, type, id, accounts, next) => { - return state.updateIn([type, id], map => { - return map.set('next', next).update('items', list => list.push(...accounts.map(item => item.id))); - }); -}; - -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_REQUESTS_EXPAND_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.push(...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)); - case BLOCKS_FETCH_SUCCESS: - return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); - case BLOCKS_EXPAND_SUCCESS: - return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); - case MUTES_FETCH_SUCCESS: - return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); - case MUTES_EXPAND_SUCCESS: - return state.updateIn(['mutes', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); - default: - return state; - } -}; diff --git a/app/assets/javascripts/components/rtl.jsx b/app/assets/javascripts/components/rtl.jsx @@ -1,27 +0,0 @@ -// U+0590 to U+05FF - Hebrew -// U+0600 to U+06FF - Arabic -// U+0700 to U+074F - Syriac -// U+0750 to U+077F - Arabic Supplement -// U+0780 to U+07BF - Thaana -// U+07C0 to U+07FF - N'Ko -// U+0800 to U+083F - Samaritan -// U+08A0 to U+08FF - Arabic Extended-A -// U+FB1D to U+FB4F - Hebrew presentation forms -// U+FB50 to U+FDFF - Arabic presentation forms A -// U+FE70 to U+FEFF - Arabic presentation forms B - -const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; - -export function isRtl(text) { - if (text.length === 0) { - return false; - } - - const matches = text.match(rtlChars); - - if (!matches) { - return false; - } - - return matches.length / text.trim().length > 0.3; -}; diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx @@ -1,72 +0,0 @@ -import { createSelector } from 'reselect'; -import Immutable from 'immutable'; - -const getStatuses = state => state.get('statuses'); -const getAccounts = state => state.get('accounts'); - -const getAccountBase = (state, id) => state.getIn(['accounts', id], null); -const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); - -export const makeGetAccount = () => { - return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => { - if (base === null) { - return null; - } - - return base.set('relationship', relationship); - }); -}; - -export const makeGetStatus = () => { - return createSelector( - [ - (state, id) => state.getIn(['statuses', id]), - (state, id) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), - (state, id) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), - (state, id) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - ], - - (statusBase, statusReblog, accountBase, accountReblog) => { - if (!statusBase) { - return null; - } - - if (statusReblog) { - statusReblog = statusReblog.set('account', accountReblog); - } else { - statusReblog = null; - } - - return statusBase.withMutations(map => { - map.set('reblog', statusReblog); - map.set('account', accountBase); - }); - } - ); -}; - -const getAlertsBase = state => state.get('alerts'); - -export const getAlerts = createSelector([getAlertsBase], (base) => { - let arr = []; - - base.forEach(item => { - arr.push({ - message: item.get('message'), - title: item.get('title'), - key: item.get('key'), - dismissAfter: 5000 - }); - }); - - return arr; -}); - -export const makeGetNotification = () => { - return createSelector([ - (_, base) => base, - (state, _, accountId) => state.getIn(['accounts', accountId]) - ], (base, account) => { - return base.set('account', account); - }); -}; diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx @@ -1,16 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import appReducer from '../reducers'; -import loadingBarMiddleware from '../middleware/loading_bar'; -import errorsMiddleware from '../middleware/errors'; -import soundsMiddleware from '../middleware/sounds'; -import Immutable from 'immutable'; - -export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( - thunk, - loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), - errorsMiddleware(), - soundsMiddleware() - ), window.devToolsExtension ? window.devToolsExtension() : f => f)); -}; diff --git a/app/assets/javascripts/components/stream.jsx b/app/assets/javascripts/components/stream.jsx @@ -1,22 +0,0 @@ -import WebSocketClient from 'websocket.js'; - -const createWebSocketURL = (url) => { - const a = document.createElement('a'); - - a.href = url; - a.href = a.href; - a.protocol = a.protocol.replace('http', 'ws'); - - return a.href; -}; - -export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { - const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); - - ws.onopen = connected; - ws.onmessage = e => received(JSON.parse(e.data)); - ws.onclose = disconnected; - ws.onreconnect = reconnected; - - return ws; -}; diff --git a/app/assets/javascripts/components/uuid.jsx b/app/assets/javascripts/components/uuid.jsx @@ -1,3 +0,0 @@ -export default function uuid(a) { - return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid); -}; diff --git a/app/assets/javascripts/extras.jsx b/app/assets/javascripts/extras.jsx @@ -1,49 +0,0 @@ -import emojify from './components/emoji'; -import { length } from 'stringz'; - -$(() => { - $.each($('.emojify'), (_, content) => { - const $content = $(content); - $content.html(emojify($content.html())); - }); - - $('.video-player video').on('click', e => { - if (e.target.paused) { - e.target.play(); - } else { - e.target.pause(); - } - }); - - $('.media-spoiler').on('click', e => { - $(e.target).hide(); - }); - - $('.webapp-btn').on('click', e => { - if (e.button === 0) { - e.preventDefault(); - window.location.href = $(e.target).attr('href'); - } - }); - - $('.status__content__spoiler-link').on('click', e => { - e.preventDefault(); - const contentEl = $(e.target).parent().parent().find('div'); - - if (contentEl.is(':visible')) { - contentEl.hide(); - $(e.target).parent().attr('style', 'margin-bottom: 0'); - } else { - contentEl.show(); - $(e.target).parent().attr('style', null); - } - }); - - // used on /settings/profile - $('.account_display_name').on('input', e => { - $('.name-counter').text(30 - length($(e.target).val())); - }); - $('.account_note').on('input', e => { - $('.note-counter').text(160 - length($(e.target).val())); - }); -}); diff --git a/app/assets/images/.keep b/app/assets/stylesheets/.gitkeep diff --git a/app/assets/stylesheets/about.scss b/app/assets/stylesheets/about.scss @@ -1,374 +0,0 @@ -.about-body { - .wrapper { - max-width: 600px; - margin: 0 auto; - color: $color3; - padding-top: 50px; - padding-bottom: 50px; - - &.thicc { - max-width: 700px; - } - } - - h1 { - font: 46px/52px 'Roboto', sans-serif; - font-weight: 600; - margin-bottom: 20px; - color: $color4; - padding: 20px 0; - - img { - margin-bottom: -5px; - margin-right: 5px; - width: 46px; - height: 46px; - } - } - - h2 { - font-family: 'Montserrat', sans-serif; - font-size: 24px; - line-height: 28px; - font-weight: 400; - margin-bottom: 20px; - color: $color5; - } - - h3 { - font-family: 'Montserrat', sans-serif; - font-size: 20px; - line-height: 28px; - font-weight: 400; - margin-bottom: 20px; - color: $color2; - } - - ul, ol { - list-style: inherit; - margin-left: 20px; - - &[type='a'] { - list-style-type: lower-alpha; - } - - &[type='i'] { - list-style-type: lower-roman; - } - } - - li > ol, li > ul { - margin-top: 20px; - } - - p, li { - font: 16px/28px 'Montserrat', sans-serif; - font-weight: 400; - margin-bottom: 12px; - - a { - color: $color4; - text-decoration: underline; - } - } - - em { - display: inline-block; - padding: 7px 7px 5px 7px; - margin: 0 2px; - background: $color3; - color: $color1; - font: 16px/16px 'Montserrat', sans-serif; - font-weight: 300; - } - - .screenshot { - box-shadow: 0 0 15px rgba($color8, 0.4); - margin-bottom: 26px; - - img { - max-width: 100%; - height: auto; - display: block; - } - } - - .actions { - overflow: hidden; - margin-bottom: 20px; - - .info { - float: right; - text-align: right; - line-height: 36px; - - a { - color: $color3; - text-decoration: underline; - } - } - } - - @media screen and (max-width: 625px) { - .wrapper { - padding: 20px; - } - - .features-list { - display: block; - } - } -} - -.information-board { - margin: 20px 0; - display: flex; - justify-content: space-between; - border-top: 1px solid lighten($color1, 10%); - border-bottom: 1px solid lighten($color1, 10%); - padding-right: 14px; - - .section { - flex: 1 0 0; - padding: 14px; - text-align: right; - font: 16px/28px 'Montserrat', sans-serif; - - span, strong { - display: block; - } - - span { - font-size: 16px; - - &:last-child { - color: $color2; - font-size: 14px; - } - } - - strong { - font-weight: 500; - font-size: 32px; - line-height: 48px; - color: $color5; - } - } - - @media screen and (max-width: 500px) { - flex-direction: column; - - .section { - text-align: left; - } - } -} - -.owner { - text-align: center; - - .avatar { - width: 80px; - height: 80px; - margin: 0 auto; - margin-bottom: 15px; - - img { - display: block; - width: 80px; - height: 80px; - border-radius: 48px; - } - } - - .name { - font-size: 14px; - - a { - display: block; - color: $color5; - text-decoration: none; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - - .username { - display: block; - color: $color3; - } - } -} - -.contact-email { - text-align: center; - margin: 40px 0; - - strong { - display: block; - color: $color5; - word-break: break-word; - } -} - -.sidebar-layout { - display: flex; - - .main { - flex: 1 1 auto; - padding: 14px 0; - - .panel { - padding-right: 14px; - } - } - - .sidebar { - border-left: 1px solid lighten($color1, 10%); - width: 180px; - flex: 0 0 auto; - } - - .panel { - .panel-header { - background: lighten($color1, 10%); - padding: 7px 14px; - text-transform: uppercase; - font-size: 12px; - font-weight: 500; - } - - .panel-body { - padding: 14px; - } - - .panel-list { - ul { - list-style: none; - margin: 0; - - li { - margin: 0; - font-family: inherit; - font-size: 13px; - line-height: 18px; - - a { - display: block; - padding: 7px 14px; - color: rgba($color5, 0.7); - text-decoration: none; - transition: all 200ms linear; - - i.fa { - margin-right: 5px; - } - - &:hover { - color: $color5; - background-color: darken($color1, 5%); - transition: all 100ms linear; - } - - &.selected { - color: $color5; - background-color: $color4; - - &:hover { - background-color: lighten($color4, 5%); - } - } - } - } - } - } - } - - @media screen and (max-width: 625px) { - flex-direction: column; - - .sidebar { - border: 1px solid lighten($color1, 10%); - width: auto; - } - } -} - -.features-list { - display: flex; - margin-bottom: 20px; - - .features-list__column { - flex: 1 1 0; - - ul { - list-style: none; - } - - li { - margin: 0; - } - } -} - -.screenshot-with-signup { - display: flex; - margin-bottom: 20px; - - .mascot { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - - img { - display: block; - margin: 0 auto; - max-width: 100%; - height: auto; - } - } - - .simple_form, .closed-registrations-message { - width: 300px; - flex: 0 0 auto; - background: rgba(darken($color1, 7%), 0.5); - padding: 14px; - border-radius: 4px; - box-shadow: 0 0 15px rgba($color8, 0.4); - - .actions { - margin-bottom: 0; - } - - .info { - text-align: center; - - a { - color: $color2; - } - } - } - - @media screen and (max-width: 625px) { - .mascot { - display: none; - } - - .simple_form, .closed-registrations-message { - flex: auto; - } - } -} - -.closed-registrations-message { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; -} diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss @@ -1,391 +0,0 @@ -.card { - background: $color1; - background-size: cover; - padding: 60px 0; - padding-bottom: 0; - border-radius: 4px 4px 0 0; - box-shadow: 0 0 15px rgba($color8, 0.2); - overflow: hidden; - position: relative; - - @media screen and (max-width: 700px) { - border-radius: 0; - box-shadow: none; - } - - &:after { - background: linear-gradient(rgba($color8, 0.5), rgba($color8, 0.8)); - display: block; - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 1; - } - - .name { - display: block; - font-size: 20px; - line-height: 18px * 1.5; - color: $color5; - font-weight: 500; - text-align: center; - position: relative; - z-index: 2; - text-shadow: 0 0 2px $color8; - - small { - display: block; - font-size: 14px; - color: $color4; - font-weight: 400; - } - } - - .avatar { - width: 120px; - margin: 0 auto; - margin-bottom: 15px; - position: relative; - z-index: 2; - - img { - width: 120px; - height: 120px; - display: block; - border-radius: 120px; - } - } - - .controls { - position: absolute; - top: 10px; - right: 10px; - z-index: 2; - } - - .details { - display: flex; - margin-top: 30px; - position: relative; - z-index: 2; - flex-direction: row; - } - - .details-counters { - display: flex; - flex-direction: row; - order: 0; - } - - .counter { - width: 80px; - color: $color3; - padding: 5px 10px 0px; - margin-bottom: 10px; - border-right: 1px solid $color3; - cursor: default; - position: relative; - - a { - display: block; - } - - &:after { - display: block; - content: ""; - position: absolute; - bottom: -10px; - left: 0; - width: 100%; - border-bottom: 4px solid $color3; - opacity: 0.5; - transition: all 0.8s ease; - } - - &.active { - &:after { - border-bottom: 4px solid $color4; - opacity: 1; - } - } - - &:hover { - &:after { - opacity: 1; - transition-duration: 0.2s; - } - } - - a { - text-decoration: none; - color: inherit; - } - - .counter-label { - font-size: 12px; - text-transform: uppercase; - display: block; - margin-bottom: 5px; - text-shadow: 0 0 2px $color8; - } - - .counter-number { - font-weight: 500; - font-size: 18px; - color: $color5; - } - } - - .bio { - flex: 1; - font-size: 14px; - line-height: 18px; - padding: 5px 10px; - color: $color2; - order: 1; - } - - @media screen and (max-width: 480px) { - .details { - display: block; - } - - .bio { - text-align: center; - margin-bottom: 20px; - } - - .counter { - flex: 1 1 auto; - } - - .counter:last-child { - border-right: none; - } - } -} - -.pagination { - padding: 30px 0; - text-align: center; - overflow: hidden; - - a, .current, .next, .prev, .page, .gap { - font-size: 14px; - color: $color5; - font-weight: 500; - display: inline-block; - padding: 6px 10px; - text-decoration: none; - } - - .current { - background: $color5; - border-radius: 100px; - color: $color1; - cursor: default; - margin: 0 10px; - } - - .gap { - cursor: default; - } - - .prev, .next { - text-transform: uppercase; - color: $color2; - } - - .prev { - float: left; - padding-left: 0; - - .fa { - display: inline-block; - margin-right: 5px; - } - } - - .next { - float: right; - padding-right: 0; - - .fa { - display: inline-block; - margin-left: 5px; - } - } - - .disabled { - cursor: default; - color: lighten($color1, 10%); - } - - @media screen and (max-width: 360px) { - padding: 30px 20px; - - a, .current, .next, .prev, .gap { - display: none; - } - - .next, .prev { - display: inline-block; - } - } -} - -.accounts-grid { - box-shadow: 0 0 15px rgba($color8, 0.2); - background: $color5; - border-radius: 0 0 4px 4px; - padding: 20px 10px; - padding-bottom: 10px; - overflow: hidden; - display: flex; - flex-wrap: wrap; - - @media screen and (max-width: 700px) { - border-radius: 0; - box-shadow: none; - } - - .account-grid-card { - box-sizing: border-box; - width: 335px; - border: 1px solid $color2; - border-radius: 4px; - color: $color1; - margin-bottom: 10px; - - &:nth-child(odd) { - margin-right: 10px; - } - - .account-grid-card__header { - overflow: hidden; - padding: 10px; - border-bottom: 1px solid $color2; - } - - .avatar { - width: 60px; - height: 60px; - float: left; - margin-right: 15px; - - img { - display: block; - width: 60px; - height: 60px; - border-radius: 60px; - } - } - - .name { - padding-top: 10px; - - a { - display: block; - color: $color1; - text-decoration: none; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - } - - .display_name { - font-size: 14px; - display: block; - } - - .username { - color: $color4; - } - - .note { - padding: 10px; - padding-top: 15px; - color: $color3; - word-wrap: break-word; - } - } -} - -.nothing-here { - color: $color3; - font-size: 14px; - font-weight: 500; - text-align: center; - padding: 15px 0; - padding-bottom: 25px; - cursor: default; -} - -.account-card { - padding: 14px 10px; - background: $color5; - border-radius: 4px; - text-align: left; - box-shadow: 0 0 15px rgba($color8, 0.2); - - .detailed-status__display-name { - display: block; - overflow: hidden; - margin-bottom: 15px; - - &:last-child { - margin-bottom: 0; - } - - & > div { - float: left; - margin-right: 10px; - width: 48px; - height: 48px; - } - - .avatar { - display: block; - border-radius: 4px; - } - - .display-name { - display: block; - max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: default; - - strong { - font-weight: 500; - color: $color1; - } - - span { - font-size: 14px; - color: $color3; - } - } - - &:hover { - .display-name { - strong { - text-decoration: none; - } - } - } - } - - .account__header__content { - font-size: 14px; - color: $color1; - } -} diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss @@ -1,245 +0,0 @@ -.admin-wrapper { - display: flex; - justify-content: center; - height: 100%; - - .sidebar-wrapper { - flex: 1; - height: 100%; - background: $color1; - display: flex; - justify-content: flex-end; - } - - .sidebar { - width: 240px; - height: 100%; - padding: 0; - overflow-y: auto; - - .logo { - display: block; - margin: 40px auto; - width: 100px; - height: 100px; - } - - ul { - list-style: none; - border-radius: 4px 0 0 4px; - overflow: hidden; - margin-bottom: 20px; - - a { - display: block; - padding: 15px 25px; - color: rgba($color5, 0.7); - text-decoration: none; - transition: all 200ms linear; - border-radius: 4px 0 0 4px; - - i.fa { - margin-right: 5px; - } - - &:hover { - color: $color5; - background-color: darken($color1, 5%); - transition: all 100ms linear; - } - - &.selected { - background: darken($color1, 2%); - border-radius: 4px 0 0 0; - } - } - - ul { - background: darken($color1, 4%); - border-radius: 0 0 0 4px; - margin: 0; - - a { - border: 0; - - &.selected { - color: $color5; - background-color: $color4; - border-bottom: 0; - border-radius: 0; - - &:hover { - background-color: lighten($color4, 5%); - } - } - } - } - } - } - - .content-wrapper { - flex: 2; - overflow: auto; - } - - .content { - max-width: 700px; - padding: 20px 15px; - padding-top: 60px; - padding-left: 25px; - - h2 { - color: $color2; - font-size: 24px; - line-height: 28px; - font-weight: 400; - margin-bottom: 40px; - } - - & > p { - font-size: 14px; - line-height: 18px; - color: $color2; - margin-bottom: 20px; - - strong { - color: $color5; - font-weight: 500; - } - } - - hr { - margin: 20px 0; - border: 0; - background: transparent; - border-bottom: 1px solid $color1; - } - } - - .simple_form { - max-width: 400px; - .label_input { - label.select { - width: 50%; - } - select { - width: 50%; - float: right; - } - } - } - - @media screen and (max-width: 600px) { - display: block; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - - .sidebar-wrapper, .content-wrapper { - flex: 0 0 auto; - height: auto; - overflow: initial; - } - - .sidebar { - width: 100%; - padding: 10px 0; - height: auto; - - .logo { - margin: 20px auto; - } - } - - .content { - padding-top: 20px; - } - } -} - -.filters { - display: flex; - margin-bottom: 20px; - - .filter-subset { - flex: 0 0 auto; - margin-right: 40px; - - ul { - margin-top: 5px; - list-style: none; - - li { - display: inline-block; - margin-right: 5px; - } - } - - strong { - font-weight: 500; - text-transform: uppercase; - font-size: 12px; - } - - a { - display: inline-block; - color: rgba($color5, 0.7); - text-decoration: none; - text-transform: uppercase; - font-size: 12px; - font-weight: 500; - border-bottom: 2px solid $color1; - - &:hover { - color: $color5; - border-bottom: 2px solid lighten($color1, 5%); - } - - &.selected { - color: $color4; - border-bottom: 2px solid $color4; - } - } - } -} - -.report-accounts { - display: flex; - margin-bottom: 20px; -} - -.report-accounts__item { - flex: 1 1 0; - display: flex; - flex-direction: column; - - & > strong { - display: block; - margin-bottom: 10px; - font-weight: 500; - font-size: 14px; - line-height: 18px; - color: $color2; - } - - &:first-child { - margin-right: 10px; - } - - .account-card { - flex: 1 1 auto; - } -} - -.report-status { - display: flex; - margin-bottom: 10px; - - .activity-stream { - flex: 2 0 0; - margin-right: 20px; - } -} - -.report-status__actions { - flex: 0 0 auto; -} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss @@ -1,21 +0,0 @@ -@import 'variables'; -@import 'fonts/roboto'; -@import 'fonts/roboto-mono'; -@import 'fonts/montserrat'; -@import 'font-awesome'; - -@import 'reset'; -@import 'basics'; -@import 'containers'; -@import 'lists'; -@import 'footer'; -@import 'compact_header'; -@import 'landing_strip'; -@import 'forms'; -@import 'accounts'; -@import 'stream_entries'; -@import 'components'; -@import 'about'; -@import 'tables'; -@import 'admin'; -@import 'rtl'; diff --git a/app/assets/stylesheets/basics.scss b/app/assets/stylesheets/basics.scss @@ -1,58 +0,0 @@ -body { - font-family: 'Roboto', sans-serif; - background: $color1 image-url('background-photo.jpg'); - background-size: cover; - background-attachment: fixed; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $color5; - padding-bottom: 140px; - text-rendering: optimizelegibility; - font-feature-settings: "kern"; - text-size-adjust: none; - - &.app-body { - position: fixed; - width: 100%; - height: 100%; - padding: 0; - background: $color1; - } - - &.embed { - background: transparent; - margin: 0; - - .container { - position: absolute; - width: 100%; - height: 100%; - overflow: hidden; - } - } - - &.admin { - background: darken($color1, 4%); - position: fixed; - width: 100%; - height: 100%; - padding: 0; - } - - @media screen and (max-width: 360px) { - padding-bottom: 0; - } -} - -button:focus { - outline: none; -} - -.app-holder { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; -} diff --git a/app/assets/stylesheets/boost.scss b/app/assets/stylesheets/boost.scss @@ -1,11 +0,0 @@ -@function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) -} - -button.icon-button i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($color1, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($color4)}' stroke-width='0'/></svg>"); - - &:hover { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($color1, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($color4)}' stroke-width='0'/></svg>"); - } -} diff --git a/app/assets/stylesheets/compact_header.scss b/app/assets/stylesheets/compact_header.scss @@ -1,28 +0,0 @@ -.compact-header { - h1 { - font-size: 24px; - line-height: 28px; - color: $color3; - overflow: hidden; - font-weight: 500; - margin-bottom: 20px; - - a { - color: inherit; - text-decoration: none; - } - - small { - font-weight: 400; - color: $color2; - } - - img { - display: inline-block; - margin-bottom: -5px; - margin-right: 15px; - width: 36px; - height: 36px; - } - } -} diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss @@ -1,3180 +0,0 @@ -@import 'variables'; - -.app-body { - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; -} - -.button { - background-color: darken($color4, 3%); - border: 10px none; - border-radius: 4px; - box-sizing: border-box; - color: $color5; - cursor: pointer; - display: inline-block; - font-family: inherit; - font-size: 14px; - font-weight: 500; - height: 36px; - letter-spacing: 0; - line-height: 36px; - overflow: hidden; - padding: 0 16px; - position: relative; - text-align: center; - text-transform: uppercase; - text-decoration: none; - text-overflow: ellipsis; - transition: all 100ms ease-in; - white-space: nowrap; - - &:active, - &:focus, - &:hover { - background-color: lighten($color4, 7%); - transition: all 200ms ease-out; - } - - &:disabled { - background-color: $color3; - cursor: default; - } - - &.button-secondary { - // - } -} - -.column-collapsable { - position: relative; -} - -.column-icon { - background: lighten($color1, 4%); - color: $color3; - cursor: pointer; - font-size: 16px; - padding: 15px; - position: absolute; - right: 0; - top: -48px; - z-index: 3; - - &:hover { - color: lighten($color3, 7%); - } -} - -.column-icon-clear { - font-size: 16px; - padding: 15px; - position: absolute; - right: 48px; - top: 0; - cursor: pointer; - z-index: 2; -} - -@media screen and (min-width: 1025px) { - .column-icon-clear { - top: 10px; - } -} - -.icon-button { - display: inline-block; - padding: 0; - color: lighten($color1, 26%); - border: none; - background: transparent; - cursor: pointer; - transition: color 100ms ease-in; - - &:hover, &:active, &:focus { - color: lighten($color1, 33%); - transition: color 200ms ease-out; - } - - &.disabled { - color: lighten($color1, 13%); - cursor: default; - } - - &.active { - color: $color4; - } - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, &:focus, &:active { - outline: 0 !important; - } - - &.inverted { - color: lighten($color1, 33%); - - &:hover, &:active, &:focus { - color: lighten($color1, 26%); - } - - &.active { - color: $color4; - } - - &.disabled { - color: $color3; - } - } - - &.overlayed { - box-sizing: content-box; - background: rgba($color8, 0.6); - color: rgba($color5, 0.7); - border-radius: 4px; - padding: 2px; - - &:hover { - background: rgba($color8, 0.9); - } - } -} - -.text-icon-button { - color: lighten($color1, 33%); - border: none; - background: transparent; - cursor: pointer; - font-weight: 600; - font-size: 11px; - padding: 0 3px; - line-height: 27px; - outline: 0; - transition: color 100ms ease-in; - - &:hover, &:active, &:focus { - color: lighten($color1, 26%); - transition: color 200ms ease-out; - } - - &.disabled { - color: lighten($color1, 13%); - cursor: default; - } - - &.active { - color: $color4; - } - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, &:focus, &:active { - outline: 0 !important; - } -} - -.dropdown--active .icon-button { - color: $color4; -} - -.dropdown--active:after { - content: ""; - display: block; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-width: 0 4.5px 7.8px 4.5px; - border-color: transparent transparent $color2 transparent; - bottom: 8px; - right: 104px; -} - -.invisible { - font-size: 0; - line-height: 0; - display: inline-block; - width: 0; -} - -.ellipsis { - &:after { - content: "…"; - } -} - -.lightbox .icon-button { - color: $color1; -} - -.compose-form { - padding: 10px; -} - -.compose-form__warning { - color: darken($color3, 33%); - margin-bottom: 15px; - background: $color3; - box-shadow: 0 2px 6px rgba($color8, 0.3); - padding: 8px 10px; - border-radius: 4px; - font-size: 13px; - font-weight: 400; - - strong { - color: darken($color3, 33%); - font-weight: 500; - } - - a { - color: darken($color3, 33%); - font-weight: 500; - text-decoration: underline; - - &:hover, &:active, &:focus { - text-decoration: none; - } - } -} - -.compose-form__modifiers { - color: $color1; - font-family: inherit; - font-size: 14px; - background: $color5; - border-radius: 0 0 4px 0; -} - -.compose-form__buttons-wrapper { - display: flex; - justify-content: space-between; -} - -.compose-form__buttons { - padding: 10px; - background: darken($color5, 8%); - box-shadow: inset 0 5px 5px rgba($color8, 0.05); - border-radius: 0 0 4px 4px; - display: flex; - - .icon-button { - box-sizing: content-box; - padding: 0 3px; - } -} - -.compose-form__upload-button-icon { - line-height: 27px; -} - -.compose-form__upload-wrapper { - overflow: hidden; -} - -.compose-form__uploads-wrapper { - display: flex; - padding: 5px; -} - -.compose-form__upload { - flex: 1 1 0; - margin: 5px; -} - -.compose-form__upload-thumbnail { - border-radius: 4px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - height: 100px; - width: 100%; -} - -.compose-form__upload-cancel { - background-size: cover; - border-radius: 4px; - height: 100px; - width: 100px; -} - -.compose-form__label { - display: block; - line-height: 24px; - vertical-align: middle; - - &.with-border { - border-top: 1px solid $color1; - padding-top: 10px; - } - - .compose-form__label__text { - display: inline-block; - vertical-align: middle; - margin-bottom: 14px; - margin-left: 8px; - color: $color3; - } -} - -.compose-form__textarea, .follow-form__input { - background: $color5; - - &:disabled { - background: $color2; - } -} - -.compose-form__autosuggest-wrapper { - position: relative; - - .dropdown--active:after { - border-color: transparent transparent $color5 transparent; - bottom: -1px; - right: 8px; - } -} - -.compose-form__publish { - display: flex; - min-width: 0; -} - -.compose-form__publish-button-wrapper { - overflow: hidden; - padding-top: 10px; -} - -.emojione { - display: inline-block; - font-size: inherit; - vertical-align: middle; - margin: -.2ex .15em .2ex; - width: 16px; - height: 16px; - - img { - width: auto; - } -} - -.reply-indicator { - border-radius: 4px 4px 0 0; - position: relative; - bottom: -2px; - background: $color3; - padding: 10px; -} - -.reply-indicator__header { - margin-bottom: 5px; - overflow: hidden; -} - -.reply-indicator__cancel { - float: right; - line-height: 24px; -} - -.reply-indicator__display-name { - color: $color1; - display: block; - max-width: 100%; - line-height: 24px; - overflow: hidden; - padding-right: 25px; - text-decoration: none; -} - -.reply-indicator__display-avatar { - float: left; - margin-right: 5px; -} - -.status__content { - cursor: pointer; -} - -.status__content--no-action { - cursor: default; -} - -.status__content, -.reply-indicator__content { - font-size: 15px; - line-height: 20px; - word-wrap: break-word; - font-weight: 400; - overflow: hidden; - white-space: pre-wrap; - - .emojione { - width: 18px; - height: 18px; - } - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - color: $color2; - text-decoration: none; - - &:hover { - text-decoration: underline; - - .fa { - color: lighten($color1, 40%); - } - } - - &.mention { - &:hover { - text-decoration: none; - - span { - text-decoration: underline; - } - } - } - - .fa { - color: lighten($color1, 30%); - } - } - - .status__content__spoiler-link { - background: lighten($color1, 30%); - - &:hover { - background: lighten($color1, 33%); - text-decoration: none; - } - } -} - -a.status__content__spoiler-link { - display: inline-block; - border-radius: 2px; - color: lighten($color1, 8%); - font-weight: 500; - font-size: 11px; - padding: 0px 6px; - text-transform: uppercase; - line-height: inherit; -} - -.status__prepend-icon-wrapper { - left: -26px; - position: absolute; -} - -.status { - padding: 8px 10px; - padding-left: 68px; - position: relative; - min-height: 48px; - border-bottom: 1px solid lighten($color1, 8%); - cursor: default; - - &.light { - .status__relative-time { - color: $color3; - } - - .status__display-name { - color: $color1; - } - - .display-name { - strong { - color: $color1; - } - - span { - color: $color3; - } - } - - .status__content { - color: $color1; - - a { - color: $color4; - } - - a.status__content__spoiler-link { - color: $color5; - background: $color3; - - &:hover { - background: lighten($color3, 8%); - } - } - } - } -} - -.status__relative-time { - color: lighten($color1, 26%); -} - -.status__display-name { - color: lighten($color1, 26%); -} - -.status__info .status__display-name { - display: block; - max-width: 100%; - padding-right: 25px; -} - -.status__info { - font-size: 15px; -} - -.status__info-time { - float: right; - font-size: 14px; -} - -.status-check-box { - border-bottom: 1px solid lighten($color1, 8%); - display: flex; - - .status__content { - background: lighten($color1, 4%); - flex: 1 1 auto; - padding: 10px; - } -} - -.status-check-box-toggle { - align-items: center; - display: flex; - flex: 0 0 auto; - justify-content: center; - padding: 10px; -} - -.status__prepend { - margin-left: 68px; - color: lighten($color1, 26%); - padding: 8px 0; - padding-bottom: 2px; - font-size: 14px; - position: relative; - - .status__display-name strong { - color: lighten($color1, 26%); - } -} - -.status__action-bar { - align-items: center; - display: flex; - margin-top: 10px; -} - -.status__action-bar-button-wrapper { - float: left; - margin-right: 18px; -} - -.status__action-bar-dropdown { - float: left; - height: 18px; - width: 18px; -} - -.detailed-status { - background: lighten($color1, 4%); - padding: 14px 10px; - - .status__content { - font-size: 19px; - line-height: 24px; - - .emojione { - width: 22px; - height: 22px; - } - } -} - -.detailed-status__meta { - margin-top: 15px; - color: lighten($color1, 26%); - font-size: 14px; - line-height: 18px; -} - -.detailed-status__action-bar { - background: lighten($color1, 4%); - border-top: 1px solid lighten($color1, 8%); - border-bottom: 1px solid lighten($color1, 8%); - display: flex; - flex-direction: row; - padding: 10px 0; -} - -.detailed-status__link { - color: inherit; - text-decoration: none; -} - -.detailed-status__favorites, -.detailed-status__reblogs { - display: inline-block; - font-weight: 500; - font-size: 12px; - margin-left: 6px; -} - -.reply-indicator__content { - color: $color1; - font-size: 14px; - - a { - color: lighten($color1, 20%); - } -} - -.account { - padding: 10px; - border-bottom: 1px solid lighten($color1, 8%); - - .account__display-name { - flex: 1 1 auto; - display: block; - color: $color3; - overflow: hidden; - text-decoration: none; - font-size: 14px; - } -} - -.account__wrapper { - display: flex; -} - -.account__avatar-wrapper { - float: left; - margin-left: 12px; - margin-right: 12px; -} - -.account__avatar { - border-radius: 4px; - background: transparent no-repeat; - background-position: 50%; - background-clip: padding-box; - position: relative; -} - -.account__relationship { - height: 18px; - padding: 10px; -} - -.account__header { - flex: 0 0 auto; - background: lighten($color1, 4%); - text-align: center; - background-size: cover; - background-position: center; - position: relative; - - & > div { - background: rgba(lighten($color1, 4%), 0.9); - } - - .account__header__content { - color: $color2; - } - - .account__header__display-name { - color: $color5; - } - - .account__header__username { - color: $color4; - } -} - -.account__header__content { - color: $color3; - font-size: 14px; - font-weight: 400; - overflow: hidden; - word-break: normal; - word-wrap: break-word; - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - color: inherit; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } -} - -.account__header__display-name { - .emojione { - width: 25px; - height: 25px; - } -} - -.account__action-bar { - border-top: 1px solid lighten($color1, 8%); - border-bottom: 1px solid lighten($color1, 8%); - line-height: 36px; - overflow: hidden; - flex: 0 0 auto; - display: flex; -} - -.account__action-bar-dropdown { - flex: 1 1 auto; - padding: 10px; - - .dropdown--active { - .dropdown__content.dropdown__right { - left: 6px; - right: initial; - } - - &:after { - bottom: initial; - margin-left: 11px; - margin-top: -7px; - right: initial; - } - } -} - -.account__action-bar-links { - display: flex; - flex: 1 1 auto; - line-height: 18px; -} - -.account__action-bar__tab { - text-decoration: none; - overflow: hidden; - width: 80px; - border-left: 1px solid lighten($color1, 8%); - padding: 10px 5px; - - & > span { - display: block; - text-transform: uppercase; - font-size: 11px; - color: $color3; - } - - strong { - display: block; - font-size: 15px; - font-weight: 500; - color: $color5; - } - - abbr { - color: lighten($color1, 26%); - } -} - -.account__header__avatar { - background-size: 90px 90px; - display: block; - height: 90px; - margin: 0 auto 10px; - overflow: hidden; - width: 90px; -} - -.account-authorize { - padding: 14px 10px; - - .detailed-status__display-name { - display: block; - margin-bottom: 15px; - overflow: hidden; - } -} - -.account-authorize__avatar { - float: left; - margin-right: 10px; -} - -.status__display-name, -.status__relative-time, -.detailed-status__display-name, -.detailed-status__datetime, -.detailed-status__application, -.account__display-name { - text-decoration: none; -} - -.status__display-name, -.account__display-name { - strong { - color: $color5; - } - - &.muted { - .emojione { - opacity: 0.5; - } - } -} - -.status__display-name, -.reply-indicator__display-name, -.detailed-status__display-name, -.account__display-name { - &:hover strong { - text-decoration: underline; - } -} - -.account__display-name strong { - display: block; -} - -.detailed-status__application, -.detailed-status__datetime { - color: inherit; -} - -.detailed-status__display-name { - color: $color2; - display: block; - line-height: 24px; - margin-bottom: 15px; - overflow: hidden; - - strong, - span { - display: block; - } - - strong { - font-size: 16px; - color: $color5; - } -} - -.detailed-status__display-avatar { - float: left; - margin-right: 10px; -} - -.status__avatar { - height: 48px; - left: 10px; - position: absolute; - top: 10px; - width: 48px; -} - -.muted { - .status__content p, - .status__content a { - color: lighten($color1, 26%); - } - - .status__display-name strong { - color: lighten($color1, 26%); - } - - .status__avatar { - opacity: 0.5; - } - - a.status__content__spoiler-link { - background: lighten($color1, 26%); - color: lighten($color1, 4%); - - &:hover { - background: lighten($color1, 29%); - text-decoration: none; - } - } -} - -.notification__message { - margin-left: 68px; - padding: 8px 0; - padding-bottom: 0; - cursor: default; - color: $color3; - font-size: 15px; - position: relative; - - .fa { - color: $color4; - } -} - -.notification__favourite-icon-wrapper { - left: -26px; - position: absolute; - - .star-icon { - color: #ca8f04; - } -} - -.star-icon.active { - color: #ca8f04; -} - -.notification__display-name { - color: inherit; - font-weight: 500; - text-decoration: none; - - &:hover { - color: $color5; - text-decoration: underline; - } -} - -.display-name { - display: block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.display-name__html { - font-weight: 500; -} - -.display-name__account { - font-size: 14px; -} - -.status__relative-time, -.detailed-status__datetime { - &:hover { - text-decoration: underline; - } -} - -.transparent-background, .imageloader { - background: image-url('void.png'); -} - -.imageloader { - display: block; -} - -.navigation-bar { - padding: 10px; - display: flex; - flex-shrink: 0; - cursor: default; - color: $color3; - - strong { - color: $color5; - } - - .permalink { - text-decoration: none; - } -} - -.navigation-bar__profile { - flex: 1 1 auto; - margin-left: 8px; -} - -.navigation-bar__profile-account { - display: block; - font-weight: 500; -} - -.navigation-bar__profile-edit { - color: inherit; - text-decoration: none; -} - -.dropdown { - display: inline-block; -} - -.dropdown__content { - display: none; - position: absolute; -} - -.dropdown__sep { - border-bottom: 1px solid darken($color2, 8%); - margin: 5px 7px 6px; - padding-top: 1px; -} - -.dropdown--active .dropdown__content { - display: block; - line-height: 18px; - max-width: 311px; - right: 0; - text-align: left; - z-index: 9999; - - & > ul { - list-style: none; - background: $color2; - padding: 4px 0; - border-radius: 4px; - box-shadow: 0 0 15px rgba($color8, 0.4); - min-width: 140px; - position: relative; - } - - &.dropdown__right { - right: 0; - } - - &.dropdown__left { - & > ul { - left: -98px; - } - } - - & > ul > li > a { - font-size: 13px; - line-height: 18px; - display: block; - padding: 4px 14px; - box-sizing: border-box; - text-decoration: none; - background: $color2; - color: $color1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:focus { - outline: 0; - } - - &:hover { - background: $color4; - color: $color2; - } - } -} - -.dropdown__icon { - vertical-align: middle; -} - -.static-content { - padding: 10px; - padding-top: 20px; - color: lighten($color1, 26%); - - h1 { - font-size: 16px; - font-weight: 500; - margin-bottom: 40px; - text-align: center; - } - - p { - font-size: 13px; - margin-bottom: 20px; - } -} - -.columns-area { - display: flex; - flex: 1 1 auto; - flex-direction: row; - justify-content: flex-start; - overflow-x: auto; - position: relative; -} - -@media screen and (min-width: 360px) { - .columns-area { - padding: 10px; - } -} - -.column { - width: 330px; - position: relative; - box-sizing: border-box; - display: flex; - flex-direction: column; - - > .scrollable { - background: $color1; - } -} - -.ui { - flex: 0 0 auto; - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - background: darken($color1, 7%); -} - -.drawer { - width: 300px; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow-y: hidden; -} - -.drawer__tab { - display: block; - flex: 1 1 auto; - padding: 15px; - padding-bottom: 13px; - color: $color3; - text-decoration: none; - text-align: center; - font-size: 16px; - border-bottom: 2px solid transparent; -} - -.column, .drawer { - flex: 1 1 100%; - overflow: hidden; -} - -@media screen and (min-width: 360px) { - .tabs-bar { - margin: 10px; - margin-bottom: 0; - } - - .search { - margin-bottom: 10px; - } -} - -@media screen and (max-width: 1024px) { - .column, .drawer { - width: 100%; - padding: 0; - } - - .columns-area { - flex-direction: column; - } - - .search__input, .autosuggest-textarea__textarea { - font-size: 16px; - } -} - -@media screen and (min-width: 1025px) { - .columns-area { - padding: 0; - } - - .column, .drawer { - flex: 0 0 auto; - padding: 10px; - padding-left: 5px; - padding-right: 5px; - - &:first-child { - padding-left: 10px; - } - - &:last-child { - padding-right: 10px; - } - } - - .columns-area > div { - .column, .drawer { - padding-left: 5px; - padding-right: 5px; - } - } -} - -@media screen and (min-width: 1397px) { /* Width of 4 columns with margins */ - .columns-area { - margin-left: auto; - margin-right: auto; - } -} - -@media screen and (min-width: 1900px) { - .column, .drawer { - width: 400px; - border-radius: 4px; - height: 96vh; - margin-top: 2vh; - } -} - -.drawer__pager { - box-sizing: border-box; - padding: 0; - flex-grow: 1; - position: relative; - overflow: hidden; - display: flex; -} - -.drawer__inner { - position: absolute; - top: 0; - left: 0; - background: lighten($color1, 13%); - box-sizing: border-box; - padding: 0; - display: flex; - flex-direction: column; - overflow: hidden; - overflow-y: auto; - width: 100%; - height: 100%; - - &.darker { - background: $color1; - } -} - -.pseudo-drawer { - background: lighten($color1, 13%); - font-size: 13px; - text-align: left; -} - -.drawer__header { - flex: 0 0 auto; - font-size: 16px; - background: lighten($color1, 8%); - margin-bottom: 10px; - display: flex; - flex-direction: row; - - a { - transition: background 100ms ease-in; - - &:hover { - background: lighten($color1, 3%); - transition: background 200ms ease-out; - } - } -} - -.tabs-bar { - display: flex; - background: lighten($color1, 8%); - flex: 0 0 auto; - overflow-y: auto; -} - -.tabs-bar__link { - display: block; - flex: 1 1 auto; - padding: 15px 10px; - color: $color5; - text-decoration: none; - text-align: center; - font-size: 14px; - font-weight: 500; - border-bottom: 2px solid lighten($color1, 8%); - transition: all 200ms linear; - - .fa { - font-weight: 400; - font-size: 16px; - } - - &.active { - border-bottom: 2px solid $color4; - color: $color4; - } - - &:hover, &:focus, &:active { - background: lighten($color1, 14%); - transition: all 100ms linear; - } - - span { - margin-left: 5px; - display: none; - } -} - -@media screen and (min-width: 600px) { - .tabs-bar__link { - span { - display: inline; - } - } -} - -@media screen and (min-width: 1025px) { - .tabs-bar { - display: none; - } -} - -.react-autosuggest__container { - position: relative; -} - -.react-autosuggest__suggestions-container { - position: absolute; - top: 100%; - width: 100%; - z-index: 99; - box-shadow: 0 0 15px rgba($color8, 0.4); -} - -.react-autosuggest__section-title { - background: $color3; - padding: 4px 10px; - font-weight: 500; - cursor: default; - color: $color1; - text-transform: uppercase; - font-size: 11px; -} - -.react-autosuggest__suggestions-list { - background: $color2; - color: $color1; - font-size: 14px; -} - -.react-autosuggest__suggestion { - padding: 10px; - cursor: pointer; -} - -.react-autosuggest__suggestion--focused { - background: $color4; - color: $color5; -} - -.scrollable { - overflow-y: scroll; - overflow-x: hidden; - flex: 1 1 auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; - - &.optionally-scrollable { - overflow-y: auto; - } -} - -.column-back-button { - background: lighten($color1, 4%); - color: $color4; - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - padding: 15px; - z-index: 3; - - &:hover { - text-decoration: underline; - } -} - -.column-back-button__icon { - display: inline-block; - margin-right: 5px; -} - -.column-back-button--slim { - position: relative; -} - -.column-back-button--slim-button { - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - padding: 15px; - position: absolute; - right: 0; - top: -48px; -} - -.react-toggle { - display: inline-block; - position: relative; - cursor: pointer; - background-color: transparent; - border: 0; - padding: 0; - user-select: none; - -webkit-tap-highlight-color: rgba($color8, 0); - -webkit-tap-highlight-color: transparent; -} - -.react-toggle-screenreader-only { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.react-toggle--disabled { - cursor: not-allowed; - opacity: 0.5; - transition: opacity 0.25s; -} - -.react-toggle-track { - width: 50px; - height: 24px; - padding: 0; - border-radius: 30px; - background-color: $color1; - transition: all 0.2s ease; -} - -.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: darken($color1, 10%); -} - -.react-toggle--checked .react-toggle-track { - background-color: $color4; -} - -.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: lighten($color4, 10%); -} - -.react-toggle-track-check { - position: absolute; - width: 14px; - height: 10px; - top: 0px; - bottom: 0px; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - left: 8px; - opacity: 0; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-check { - opacity: 1; - transition: opacity 0.25s ease; -} - -.react-toggle-track-x { - position: absolute; - width: 10px; - height: 10px; - top: 0px; - bottom: 0px; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - right: 10px; - opacity: 1; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-x { - opacity: 0; -} - -.react-toggle-thumb { - transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; - position: absolute; - top: 1px; - left: 1px; - width: 22px; - height: 22px; - border: 1px solid $color1; - border-radius: 50%; - background-color: darken($color5, 2%); - box-sizing: border-box; - transition: all 0.25s ease; -} - -.react-toggle--checked .react-toggle-thumb { - left: 27px; - border-color: $color4; -} - -.column-link { - background: lighten($color1, 8%); - color: $color5; - display: block; - font-size: 16px; - padding: 15px; - text-decoration: none; - - &:hover { - background: lighten($color1, 11%); - } - - &.hidden-on-mobile { - @media screen and (max-width: 1024px) { - display: none; - } - } -} - -.column-link__icon { - display: inline-block; - margin-right: 5px; -} - -.column-subheading { - background: $color1; - color: lighten($color1, 26%); - padding: 8px 20px; - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - cursor: default; -} - -.autosuggest-textarea, -.spoiler-input { - position: relative; -} - -.autosuggest-textarea__textarea, -.spoiler-input__input { - display: block; - box-sizing: border-box; - width: 100%; - margin: 0; - color: $color1; - padding: 10px; - font-family: inherit; - font-size: 14px; - resize: vertical; - border: 0; - outline: 0; - - &:focus { - outline: 0; - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } -} - -.spoiler-input__input { - border-radius: 4px; -} - -.autosuggest-textarea__textarea { - min-height: 100px; - background: $color5; - border-radius: 4px 4px 0 0; - padding-bottom: 0; - padding-right: 10px + 22px; - resize: none; - - @media screen and (max-width: 600px) { - height: 100px !important; // prevent auto-resize textarea - resize: vertical; - } -} - -.autosuggest-textarea__suggestions { - position: absolute; - top: 100%; - width: 100%; - z-index: 99; - box-shadow: 0 0 15px rgba($color8, 0.4); - background: $color2; - color: $color1; - font-size: 14px; -} - -.autosuggest-textarea__suggestions__item { - padding: 10px; - cursor: pointer; - - &:hover { - background: darken($color2, 10%); - } - - &.selected { - background: $color4; - color: $color5; - } -} - -.autosuggest-account { - overflow: hidden; -} - -.autosuggest-account-icon { - float: left; - margin-right: 5px; -} - -.autosuggest-status { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - strong { - font-weight: 500; - } -} - -.character-counter__wrapper { - line-height: 36px; - margin-right: 16px; - padding-top: 10px; -} - -.character-counter { - cursor: default; - font-size: 16px; -} - -.character-counter--over { - color: #ff5050; -} - -.getting-started__wrapper { - position: relative; -} - -.getting-started { - box-sizing: border-box; - padding-bottom: 235px; - background: image-url('mastodon-getting-started.png') no-repeat 0 100%/contain local; - flex: 1 0 auto; - - p { - color: $color2; - } - - a { - color: lighten($color1, 26%); - } -} - -.setting-text { - color: $color3; - background: transparent; - border: none; - border-bottom: 2px solid $color3; - box-sizing: border-box; - display: block; - font-family: inherit; - margin-bottom: 10px; - padding: 7px 0px; - width: 100%; - - &:focus, &:active { - color: $color5; - border-bottom-color: $color4; - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } -} - -@import 'boost'; - -button.icon-button i.fa-retweet { - background-position: 0 0; - height: 19px; - transition: background-position 0.9s steps(10); - transition-duration: 0s; - vertical-align: middle; - width: 22px; - - &::before { - display: none !important; - } -} - -button.icon-button.active i.fa-retweet { - transition-duration: 0.9s; - background-position: 0 100%; -} - -.status-card { - display: flex; - cursor: pointer; - font-size: 14px; - border: 1px solid lighten($color1, 8%); - border-radius: 4px; - color: lighten($color1, 26%); - margin-top: 14px; - text-decoration: none; - overflow: hidden; - - &:hover { - background: lighten($color1, 8%); - } -} - -.status-card-video, .status-card-rich, .status-card-photo { - margin-top: 14px; - overflow: hidden; - - iframe { - width: 100%; - height: auto; - } -} - -.status-card-photo { - display: block; - text-decoration: none; - - img { - display: block; - width: 100%; - height: auto; - margin: 0; - } -} - -.status-card__title { - display: block; - font-weight: 500; - margin-bottom: 5px; - color: $color3; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.status-card__content { - flex: 1 1 auto; - overflow: hidden; - padding: 14px 14px 14px 8px; -} - -.status-card__description { - color: $color3; -} - -.status-card__image { - flex: 0 0 100px; - background: lighten($color1, 8%); -} - -.status-card__image-image { - border-radius: 4px 0px 0px 4px; - display: block; - height: auto; - margin: 0; - width: 100%; -} - -.load-more { - display: block; - color: lighten($color1, 26%); - text-align: center; - padding: 15px; - text-decoration: none; - - &:hover { - background: lighten($color1, 2%); - } -} - -.missing-indicator { - text-align: center; - font-size: 16px; - font-weight: 500; - color: lighten($color1, 16%); - padding-top: 210px; - background: image-url('mastodon-not-found.png') no-repeat center -50px; - cursor: default; -} - -.column-header { - padding: 15px; - font-size: 16px; - background: lighten($color1, 4%); - flex: 0 0 auto; - cursor: pointer; - position: relative; - z-index: 2; - outline: 0; - - &.active { - box-shadow: 0 1px 0 rgba($color4, 0.3); - } - - &.active .fa { - color: $color4; - text-shadow: 0 0 10px rgba($color4, 0.4); - } - - &.hidden-on-mobile { - @media screen and (max-width: 1024px) { - display: none; - } - } - - &:focus, &:active { - outline: 0; - } -} - -.column-header__icon { - display: inline-block; - margin-right: 5px; -} - -.loading-indicator { - color: $color2; - font-size: 16px; - font-weight: 500; - padding-top: 120px; - text-align: center; -} - -.collapsable-collapsed { - color: $color3; - background: lighten($color1, 4%); -} - -.collapsable { - color: $color5; - background: lighten($color1, 8%); - - &:hover { - color: $color5; - background: lighten($color1, 8%); - } -} - -.video-error-cover { - align-items: center; - background: $color8; - color: $color5; - cursor: pointer; - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - margin-top: 8px; - position: relative; - text-align: center; - z-index: 100; -} - -.media-spoiler { - align-items: center; - background: $color8; - color: $color5; - cursor: pointer; - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - position: relative; - text-align: center; - z-index: 100; -} - -.media-spoiler__warning { - display: block; - font-size: 14px; -} - -.media-spoiler__trigger { - display: block; - font-size: 11px; - font-weight: 500; -} - -.spoiler-button { - left: 4px; - position: absolute; - text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; - top: 4px; - z-index: 100; -} - -.modal-container--preloader { - background: lighten($color1, 8%); -} - -.account--panel { - background: lighten($color1, 4%); - border-top: 1px solid lighten($color1, 8%); - border-bottom: 1px solid lighten($color1, 8%); - display: flex; - flex-direction: row; - padding: 10px 0px; -} - -.account--panel__button, -.detailed-status__button { - flex: 1 1 auto; - text-align: center; -} - -.column-settings__outer { - background: lighten($color1, 8%); - padding: 15px; -} - -.column-settings__section { - color: $color3; - cursor: default; - display: block; - font-weight: 500; - margin-bottom: 10px; -} - -.modal-container__nav { - align-items: center; - background: rgba(0, 0, 0, 0.5); - box-sizing: border-box; - color: $color5; - cursor: pointer; - display: flex; - font-size: 24px; - height: 100%; - padding: 30px 15px; - position: absolute; - top: 0; -} - -.modal-container__nav--left { - left: -61px; -} - -.modal-container__nav--right { - right: -61px; -} - -.account--follows-info { - color: $color5; -} - -.setting-toggle__label { - display: block; - line-height: 24px; - vertical-align: middle; -} - -.setting-toggle { - color: $color3; - display: inline-block; - margin-bottom: 14px; - margin-left: 8px; - vertical-align: middle; -} - -.report.scrollable { - box-sizing: border-box; - display: flex; - flex-direction: column; - max-height: 100%; -} - -.report__target { - border-bottom: 1px solid lighten($color1, 4%); - color: $color2; - flex: 0 0 auto; - padding: 10px; - - strong { - display: block; - color: $color5; - font-weight: 500; - } -} - -.report__statuses { - flex: 1 1 auto; -} - -.report__textarea-wrapper { - flex: 0 0 100px; - padding: 10px; -} - -.report__textarea { - background: transparent; - box-sizing: border-box; - border: 0; - border-bottom: 2px solid $color3; - border-radius: 2px 2px 0 0; - color: $color5; - display: block; - font-family: inherit; - font-size: 14px; - margin-bottom: 10px; - outline: 0; - padding: 7px 4px; - resize: vertical; - width: 100%; - - &:active, &:focus { - border-bottom-color: $color4; - background: rgba($color8, 0.1); - } -} - -.report__submit { - margin-top: 10px; - overflow: hidden; -} - -.report__submit-button { - float: right; -} - -.empty-column-indicator { - color: lighten($color1, 20%); - background: $color1; - text-align: center; - padding: 20px; - font-size: 15px; - font-weight: 400; - cursor: default; - display: flex; - flex: 1 1 auto; - align-items: center; - - a { - color: $color4; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.status-list__unread-indicator, .notifications__unread-indicator { - position: absolute; - top: 35px; - left: 0; - right: 0; - margin: 0 auto; - width: 60%; - pointer-events: none; - height: 28px; - z-index: 1; - background: radial-gradient(ellipse, rgba($color4, 0.23) 0%, rgba($color4, 0) 60%); -} - -.emoji-dialog { - width: 245px; - height: 270px; - background: $color5; - box-sizing: border-box; - border-radius: 4px; - overflow: hidden; - position: relative; - box-shadow: 0 0 8px rgba($color8, 0.2); - - .emojione { - margin: 0; - width: 100%; - height: auto; - } - - .emoji-dialog-header { - padding: 0 10px; - - ul { - padding: 0; - margin: 0; - list-style: none; - } - - li { - display: inline-block; - box-sizing: border-box; - padding: 10px 5px; - cursor: pointer; - border-bottom: 2px solid transparent; - - .emoji { - width: 18px; - height: 18px; - } - - img, svg { - width: 18px; - height: 18px; - filter: grayscale(100%); - } - - &:hover { - img, svg { - filter: grayscale(0); - } - } - - &.active { - border-bottom-color: $color4; - - img, svg { - filter: grayscale(0); - } - } - } - } - - .emoji-row { - box-sizing: border-box; - overflow-y: hidden; - padding-left: 10px; - - .emoji { - display: inline-block; - padding: 2.5px; - border-radius: 4px; - } - } - - .emoji-category-header { - box-sizing: border-box; - overflow-y: hidden; - padding: 10px 8px 10px 16px; - display: table; - - > * { - display: table-cell; - vertical-align: middle; - } - } - - .emoji-category-title { - font-size: 12px; - text-transform: uppercase; - font-weight: 500; - color: darken($color2, 18%); - cursor: default; - } - - .emoji-category-heading-decoration { - text-align: right; - } - - .modifiers { - list-style: none; - padding: 0; - margin: 0; - vertical-align: middle; - white-space: nowrap; - margin-top: 4px; - - li { - display: inline-block; - padding: 0 2px; - - &:last-of-type { - padding-right: 0; - } - } - - .modifier { - display: inline-block; - border-radius: 10px; - width: 15px; - height: 15px; - position: relative; - cursor: pointer; - - &.active:after { - content: ""; - display: block; - position: absolute; - width: 7px; - height: 7px; - border-radius: 10px; - border: 2px solid $color5; - top: 2px; - left: 2px; - } - } - } - - .emoji-search-wrapper { - padding: 10px; - border-bottom: 1px solid lighten($color2, 4%); - } - - .emoji-search { - font-size: 14px; - font-weight: 400; - padding: 7px 9px; - font-family: inherit; - display: block; - width: 100%; - background: rgba($color2, 0.3); - color: darken($color2, 18%); - border: 1px solid $color2; - border-radius: 4px; - } - - .emoji-categories-wrapper { - position: absolute; - top: 42px; - bottom: 0; - left: 0; - right: 0; - } - - .emoji-search-wrapper + .emoji-categories-wrapper { - top: 93px; - } - - .emoji-row .emoji { - img, svg { - transition: transform 60ms ease-in-out; - } - - &:hover { - background: lighten($color2, 3%); - - img, svg { - transform: translateZ(0) scale(1.2); - } - } - } - - .emoji { - width: 22px; - height: 22px; - cursor: pointer; - - &:focus { - outline: 0; - } - } -} - -.upload-area { - align-items: center; - background: rgba($color8, 0.8); - display: flex; - height: 100%; - justify-content: center; - left: 0; - opacity: 0; - position: absolute; - top: 0; - visibility: hidden; - width: 100%; - z-index: 2000; - - * { - pointer-events: none; - } -} - -.upload-area__drop { - width: 320px; - height: 160px; - display: flex; - box-sizing: border-box; - position: relative; - padding: 8px; -} - -.upload-area__background { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: -1; - border-radius: 4px; - background: $color1; - box-shadow: 0 0 5px rgba($color8, 0.2); -} - -.upload-area__content { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - color: $color2; - font-size: 18px; - font-weight: 500; - border: 2px dashed lighten($color1, 26%); - border-radius: 4px; -} - -.upload-progress { - padding: 10px; - color: lighten($color1, 26%); - overflow: hidden; - display: flex; - - .fa { - font-size: 34px; - margin-right: 10px; - } - - span { - font-size: 12px; - text-transform: uppercase; - font-weight: 500; - display: block; - } -} - -.upload-progess__message { - flex: 1 1 auto; -} - -.upload-progress__backdrop { - width: 100%; - height: 6px; - border-radius: 6px; - background: lighten($color1, 26%); - position: relative; - margin-top: 5px; -} - -.upload-progress__tracker { - position: absolute; - left: 0; - top: 0; - height: 6px; - background: $color4; - border-radius: 6px; -} - -.emoji-button { - outline: 0; - - &:active, &:focus { - outline: 0 !important; - } - - img { - filter: grayscale(100%); - opacity: 0.8; - display: block; - margin: 0; - width: 22px; - height: 22px; - margin-top: 2px; - } - - &:hover, &:active, &:focus { - img { - opacity: 1; - filter: none; - } - } -} - -.dropdown--active .emoji-button img { - opacity: 1; - filter: none; -} - -.privacy-dropdown { - position: relative; -} - -.privacy-dropdown__dropdown { - display: none; - position: absolute; - left: 0; - top: 27px; - width: 230px; - background: $color5; - border-radius: 0 4px 4px 4px; - z-index: 2; - overflow: hidden; -} - -.privacy-dropdown__option { - color: $color1; - padding: 10px; - cursor: pointer; - display: flex; - - &:hover, &.active { - background: $color4; - color: $color5; - - .privacy-dropdown__option__content { - color: $color5; - - strong { - color: $color5; - } - } - } - - &.active:hover { - background: lighten($color4, 4%); - } -} - -.privacy-dropdown__option__icon { - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; -} - -.privacy-dropdown__option__content { - flex: 1 1 auto; - color: darken($color3, 24%); - - strong { - font-weight: 500; - display: block; - color: $color1; - } -} - -.privacy-dropdown.active { - .privacy-dropdown__value { - background: $color5; - border-radius: 4px 4px 0 0; - box-shadow: 0 -4px 4px rgba($color8, 0.1); - } - - .privacy-dropdown__dropdown { - display: block; - box-shadow: 2px 4px 6px rgba($color8, 0.1); - } -} - -.search { - position: relative; -} - -.search__input { - padding-right: 30px; - color: $color2; - outline: 0; - box-sizing: border-box; - display: block; - width: 100%; - border: none; - padding: 10px; - padding-right: 30px; - font-family: inherit; - background: $color1; - color: $color3; - font-size: 14px; - margin: 0; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, &:focus, &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($color1, 4%); - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } -} - -.search__icon { - .fa { - position: absolute; - top: 10px; - right: 10px; - z-index: 2; - display: inline-block; - opacity: 0; - transition: all 100ms linear; - font-size: 18px; - width: 18px; - height: 18px; - color: $color2; - cursor: default; - pointer-events: none; - - &.active { - pointer-events: auto; - opacity: 0.3; - } - } - - .fa-search { - transform: translateZ(0) rotate(90deg); - - &.active { - pointer-events: none; - transform: translateZ(0) rotate(0deg); - } - } - - .fa-times-circle { - top: 11px; - transform: translateZ(0) rotate(0deg); - cursor: pointer; - - &.active { - transform: translateZ(0) rotate(90deg); - } - - &:hover { - color: $color5; - } - } -} - -.search-results__header { - color: lighten($color1, 26%); - background: lighten($color1, 2%); - border-bottom: 1px solid darken($color1, 4%); - padding: 15px 10px; - font-size: 14px; - font-weight: 500; -} - -.search-results__hashtag { - display: block; - padding: 10px; - color: $color2; - text-decoration: none; - - &:hover, &:active, &:focus { - color: lighten($color2, 4%); - text-decoration: underline; - } -} - -.modal-root__overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 9999; - opacity: 0; - background: rgba($color8, 0.7); - transform: translateZ(0px); -} - -.modal-root__container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - align-content: space-around; - z-index: 9999; - opacity: 0; - pointer-events: none; - user-select: none; -} - -.modal-root__modal { - pointer-events: auto; - display: flex; - z-index: 9999; -} - -.media-modal { - max-width: 80vw; - max-height: 80vh; - position: relative; - - img, video { - max-width: 80vw; - max-height: 80vh; - } -} - -.media-modal__close { - position: absolute; - right: 4px; - top: 4px; - z-index: 100; -} - -.onboarding-modal { - background: $color2; - color: $color1; - border-radius: 8px; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.onboarding-modal__pager { - height: 80vh; - width: 80vw; - max-width: 520px; - max-height: 420px; - position: relative; - - & > div { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - box-sizing: border-box; - padding: 25px; - display: none; - flex-direction: column; - align-items: center; - justify-content: center; - display: flex; - opacity: 0; - user-select: text; - } -} - -@media screen and (max-width: 550px) { - .onboarding-modal { - width: 100%; - height: 100%; - border-radius: 0; - } - - .onboarding-modal__pager { - width: 100%; - height: auto; - max-width: none; - max-height: none; - flex: 1 1 auto; - } -} - -.onboarding-modal__paginator { - flex: 0 0 auto; - background: darken($color2, 8%); - display: flex; - padding: 25px; - - & > div { - min-width: 33px; - } - - a { - color: darken($color2, 34%); - text-decoration: none; - font-size: 14px; - font-weight: 500; - - &:hover, &:focus, &:active { - color: darken($color2, 38%); - } - - &.onboarding-modal__done, &.onboarding-modal__next { - color: $color4; - } - } -} - -.onboarding-modal__dots { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; -} - -.onboarding-modal__dot { - width: 14px; - height: 14px; - border-radius: 14px; - background: darken($color2, 16%); - margin: 0 3px; - cursor: pointer; - - &:hover { - background: darken($color2, 18%); - } - - &.active { - cursor: default; - background: darken($color2, 24%); - } -} - -.onboarding-modal__page { - cursor: default; - line-height: 21px; - - h1 { - font-size: 18px; - font-weight: 500; - color: $color1; - margin-bottom: 20px; - } - - a { - color: $color4; - - &:hover, &:focus, &:active { - color: lighten($color4, 4%); - } - } - - p { - font-size: 16px; - color: lighten($color1, 8%); - margin-top: 10px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - strong { - font-weight: 500; - background: $color1; - color: $color2; - border-radius: 4px; - font-size: 14px; - padding: 3px 6px; - } - } -} - -.onboarding-modal__page-one { - display: flex; -} - -.onboarding-modal__page-one__elephant-friend { - background: image-url('elephant-friend.png') no-repeat center center/contain; - width: 147px; - height: 160px; - margin-right: 10px; -} - -.onboarding-modal__page-two, -.onboarding-modal__page-three, -.onboarding-modal__page-four, -.onboarding-modal__page-five { - p { - text-align: left; - } - - .figure { - background: darken($color1, 8%); - color: $color2; - margin-bottom: 20px; - border-radius: 4px; - padding: 10px; - text-align: center; - font-size: 14px; - box-shadow: 1px 2px 6px rgba($color8, 0.3); - - .onboarding-modal__image { - border-radius: 4px; - margin-bottom: 10px; - } - - &.non-interactive { - pointer-events: none; - text-align: left; - } - } -} - -.onboarding-modal__page-four__columns { - .row { - display: flex; - margin-bottom: 20px; - - & > div { - flex: 1 1 0; - margin: 0 10px; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - - p { - text-align: center; - } - } - - &:last-child { - margin-bottom: 0; - } - } - - .column-header { - color: $color5; - } -} - -.onboarding-modal__image { - border-radius: 8px; - width: 70vw; - max-width: 450px; - max-height: auto; - display: block; - margin: auto; - margin-bottom: 20px; -} - -.onboard-sliders { - display: inline-block; - max-width: 30px; - max-height: auto; - margin-left: 10px; -} - -.boost-modal, .confirmation-modal { - background: lighten($color2, 8%); - color: $color1; - border-radius: 8px; - overflow: hidden; - max-width: 90vw; - width: 480px; - position: relative; - flex-direction: column; - - .status__display-name { - display: block; - max-width: 100%; - padding-right: 25px; - } - - .status__avatar { - height: 28px; - left: 10px; - position: absolute; - top: 10px; - width: 48px; - } -} - -.boost-modal__container { - overflow-x: scroll; - padding: 10px; - - .status { - user-select: text; - border-bottom: 0; - } -} - -.boost-modal__action-bar, .confirmation-modal__action-bar { - display: flex; - background: $color2; - padding: 10px; - line-height: 36px; - - & > div { - flex: 1 1 auto; - text-align: right; - color: lighten($color1, 33%); - padding-right: 10px; - } - - .button { - flex: 0 0 auto; - } -} - -.boost-modal__status-header { - font-size: 15px; -} - -.boost-modal__status-time { - float: right; - font-size: 14px; -} - -.confirmation-modal { - max-width: 380px; -} - -.confirmation-modal__action-bar { - & > div { - text-align: left; - padding: 0 16px; - } - - a { - color: darken($color2, 34%); - text-decoration: none; - font-size: 14px; - font-weight: 500; - - &:hover, &:focus, &:active { - color: darken($color2, 38%); - } - } -} - -.confirmation-modal__container { - padding: 30px; - font-size: 16px; - text-align: center; - - strong { - font-weight: 500; - } -} - -.loading-bar { - background-color: $color4; - height: 3px; - position: absolute; - top: 0; - left: 0; -} - -.media-gallery__gifv__label { - display: block; - position: absolute; - color: $color5; - background: rgba($color8, 0.5); - bottom: 6px; - left: 6px; - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - z-index: 1; - pointer-events: none; - opacity: 0.9; - transition: opacity 0.1s ease; -} - -.media-gallery__gifv { - &.autoplay { - .media-gallery__gifv__label { - display: none; - } - } - - &:hover { - .media-gallery__gifv__label { - opacity: 1; - } - } -} - -.attachment-list { - display: flex; - font-size: 14px; - border: 1px solid lighten($color1, 8%); - border-radius: 4px; - margin-top: 14px; - overflow: hidden; -} - -.attachment-list__icon { - flex: 0 0 auto; - color: lighten($color1, 26%); - padding: 8px 18px; - cursor: default; - border-right: 1px solid lighten($color1, 8%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 26px; - - .fa { - display: block; - } -} - -.attachment-list__list { - list-style: none; - padding: 4px 0; - padding-left: 8px; - display: flex; - flex-direction: column; - justify-content: center; - - li { - display: block; - padding: 4px 0; - } - - a { - text-decoration: none; - color: lighten($color1, 26%); - font-weight: 500; - - &:hover { - text-decoration: underline; - } - } -} - -/* Media Gallery */ -.media-gallery { - box-sizing: border-box; - margin-top: 8px; - overflow: hidden; - position: relative; - width: 100%; -} - -.media-gallery__item { - border: none; - box-sizing: border-box; - display: block; - float: left; - position: relative; -} - -.media-gallery__item-thumbnail { - background-position: center; - background-repeat: no-repeat; - background-size: cover; - cursor: zoom-in; - display: block; - height: 100%; - text-decoration: none; - width: 100%; -} - -.media-gallery__gifv { - height: 100%; - overflow: hidden; - position: relative; - width: 100%; -} - -.media-gallery__item-gifv-thumbnail { - cursor: zoom-in; - height: 100%; - object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-50%); - width: 100%; - z-index: 1; -} - -.media-gallery__item-thumbnail-label { - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); - overflow: hidden; - position: absolute; -} -/* End Media Gallery */ - -/* Status Video Player */ -.status__video-player { - background: #000; - box-sizing: border-box; - cursor: default; /* May not be needed */ - margin-top: 8px; - overflow: hidden; - position: relative; -} - -.status__video-player-video { - height: 100%; - object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-35%); - width: 100%; - z-index: 1; -} - -.status__video-player-expand, -.status__video-player-mute { - color: #fff; - opacity: 0.8; - position: absolute; - right: 4px; - text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; -} - -.status__video-player-spoiler { - color: #fff; - left: 4px; - position: absolute; - text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; - top: 4px; - z-index: 100; -} - -.status__video-player-expand { - bottom: 4px; - z-index: 100; -} - -.status__video-player-mute { - top: 4px; - z-index: 5; -} - -.media-spoiler-video { - background-size: cover; - cursor: pointer; - margin-top: 8px; - position: relative; -} - -.media-spoiler-video-play-icon { - border-radius: 100px; - color: rgba(255, 255, 255, 0.8); - font-size: 36px; - left: 50%; - padding: 5px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); -} -/* End Video Player */ diff --git a/app/assets/stylesheets/containers.scss b/app/assets/stylesheets/containers.scss @@ -1,71 +0,0 @@ -.container { - width: 700px; - margin: 0 auto; - margin-top: 40px; - - @media screen and (max-width: 700px) { - width: 100%; - margin: 0; - } -} - -.mastodon-column-container { - display: flex; - height: 100%; - width: 100%; - - // 707568 - height 100% doesn't work on child of a flex item - chromium - Monorail - // https://bugs.chromium.org/p/chromium/issues/detail?id=707568 - flex: 1 1 auto; -} - -.logo-container { - max-width: 400px; - margin: 100px auto; - margin-bottom: 0; - cursor: default; - - @media screen and (max-width: 360px) { - margin: 30px auto; - } - - h1 { - display: block; - text-align: center; - color: $color5; - font-size: 48px; - font-weight: 500; - - img { - display: block; - margin: 20px auto; - width: 180px; - height: 180px; - } - - a { - color: inherit; - text-decoration: none; - outline: 0; - - img { - opacity: 0.8; - transition: opacity 0.8s ease; - } - - &:hover { - img { - opacity: 1; - transition-duration: 0.2s; - } - } - } - - small { - display: block; - font-size: 12px; - font-weight: 400; - font-family: 'Roboto Mono', monospace; - } - } -} diff --git a/app/assets/stylesheets/fonts/montserrat.scss b/app/assets/stylesheets/fonts/montserrat.scss @@ -1,11 +0,0 @@ -@font-face { - font-family: 'Montserrat'; - src: local('Montserrat'); - src: font-url('montserrat/Montserrat-Regular.eot'); - src: font-url('montserrat/Montserrat-Regular.eot?#iefix') format('embedded-opentype'), - font-url('montserrat/Montserrat-Regular.woff2') format('woff2'), - font-url('montserrat/Montserrat-Regular.woff') format('woff'), - font-url('montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; -} diff --git a/app/assets/stylesheets/fonts/roboto-mono.scss b/app/assets/stylesheets/fonts/roboto-mono.scss @@ -1,12 +0,0 @@ -@font-face { - font-family: 'Roboto Mono'; - src: local('Roboto Mono'); - src: font-url('roboto-mono/robotomono-regular-webfont.eot'); - src: font-url('roboto-mono/robotomono-regular-webfont.eot?#iefix') format('embedded-opentype'), - font-url('roboto-mono/robotomono-regular-webfont.woff2') format('woff2'), - font-url('roboto-mono/robotomono-regular-webfont.woff') format('woff'), - font-url('roboto-mono/robotomono-regular-webfont.ttf') format('truetype'), - font-url('roboto-mono/robotomono-regular-webfont.svg#roboto_monoregular') format('svg'); - font-weight: 400; - font-style: normal; -} diff --git a/app/assets/stylesheets/fonts/roboto.scss b/app/assets/stylesheets/fonts/roboto.scss @@ -1,52 +0,0 @@ -@font-face { - font-family: 'Roboto'; - src: local('Roboto'); - src: font-url('roboto/roboto-italic-webfont.eot'); - src: font-url('roboto/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'), - font-url('roboto/roboto-italic-webfont.woff2') format('woff2'), - font-url('roboto/roboto-italic-webfont.woff') format('woff'), - font-url('roboto/roboto-italic-webfont.ttf') format('truetype'), - font-url('roboto/roboto-italic-webfont.svg#roboto-italic-webfont') format('svg'); - font-weight: normal; - font-style: italic; -} - -@font-face { - font-family: 'Roboto'; - src: local('Roboto'); - src: font-url('roboto/roboto-bold-webfont.eot'); - src: local('Roboto bold'), local('roboto-bold'), - font-url('roboto/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'), - font-url('roboto/roboto-bold-webfont.woff2') format('woff2'), - font-url('roboto/roboto-bold-webfont.woff') format('woff'), - font-url('roboto/roboto-bold-webfont.ttf') format('truetype'), - font-url('roboto/roboto-bold-webfont.svg#roboto-bold-webfont') format('svg'); - font-weight: bold; - font-style: normal; -} - -@font-face { - font-family: 'Roboto'; - src: local('Roboto'); - src: font-url('roboto/roboto-medium-webfont.eot'); - src: font-url('roboto/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'), - font-url('roboto/roboto-medium-webfont.woff2') format('woff2'), - font-url('roboto/roboto-medium-webfont.woff') format('woff'), - font-url('roboto/roboto-medium-webfont.ttf') format('truetype'), - font-url('roboto/roboto-medium-webfont.svg#roboto-medium-webfont') format('svg'); - font-weight: 500; - font-style: normal; -} - -@font-face { - font-family: 'Roboto'; - src: local('Roboto'); - src: font-url('roboto/roboto-regular-webfont.eot'); - src: font-url('roboto/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'), - font-url('roboto/roboto-regular-webfont.woff2') format('woff2'), - font-url('roboto/roboto-regular-webfont.woff') format('woff'), - font-url('roboto/roboto-regular-webfont.ttf') format('truetype'), - font-url('roboto/roboto-regular-webfont.svg#roboto-regular-webfont') format('svg'); - font-weight: normal; - font-style: normal; -} diff --git a/app/assets/stylesheets/footer.scss b/app/assets/stylesheets/footer.scss @@ -1,29 +0,0 @@ -.footer { - text-align: center; - margin-top: 30px; - font-size: 12px; - color: darken($color2, 25%); - - .domain { - font-weight: 500; - - a { - color: inherit; - text-decoration: none; - } - } - - .powered-by, .single-user-login { - font-weight: 400; - - a { - color: inherit; - text-decoration: underline; - font-weight: 500; - - &:hover { - text-decoration: none; - } - } - } -} diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss @@ -1,335 +0,0 @@ -code { - font-family: 'Roboto Mono', monospace; - font-weight: 400; -} - -.form-container { - max-width: 400px; - padding: 20px; - margin: 0 auto; -} - -.simple_form { - .input { - margin-bottom: 15px; - } - - span.hint { - display: block; - color: $color3; - font-size: 12px; - margin-top: 4px; - } - - p.hint { - margin-bottom: 15px; - } - - strong { - font-weight: 500; - } - - .label_input { - display: flex; - - label { - flex: 0 0 auto; - width: 100px; - } - - input { - flex: 1 1 auto; - } - } - - .input.file, .input.select, .input.radio_buttons { - padding: 15px 0; - margin-bottom: 0; - - label { - font-family: inherit; - font-size: 16px; - color: $color5; - display: block; - padding-top: 5px; - } - } - - .fields-group { - margin-bottom: 25px; - } - - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: white; - display: block; - width: auto; - } - - .input.boolean { - margin-bottom: 5px; - - label { - font-family: inherit; - font-size: 14px; - color: white; - display: block; - width: auto; - } - - label.checkbox { - position: relative; - padding-left: 25px; - flex: 1 1 auto; - } - - input[type=checkbox] { - position: absolute; - left: 0; - top: 1px; - margin: 0; - } - - .hint { - padding-left: 25px; - margin-left: 0; - } - } - - input[type=text], input[type=number], input[type=email], input[type=password], textarea { - background: transparent; - box-sizing: border-box; - border: 0; - border-bottom: 2px solid $color3; - border-radius: 2px 2px 0 0; - padding: 7px 4px; - font-size: 16px; - color: $color5; - display: block; - width: 100%; - outline: 0; - font-family: inherit; - resize: vertical; - - &:invalid { - box-shadow: none; - } - - &:focus:invalid { - border-bottom-color: $color6; - } - - &:required:valid { - border-bottom-color: $color7; - } - - &:active, &:focus { - border-bottom-color: $color4; - background: rgba($color8, 0.1); - } - } - - .input.field_with_errors { - label { - color: $color6; - } - - input[type=text], input[type=email], input[type=password] { - border-bottom-color: $color6; - } - - .error { - display: block; - font-weight: 500; - color: $color6; - margin-top: 4px; - } - } - - .actions { - margin-top: 30px; - } - - button, .block-button { - display: block; - width: 100%; - border: 0; - border-radius: 4px; - background: $color4; - color: $color5; - font-size: 18px; - padding: 10px; - text-transform: uppercase; - text-decoration: none; - text-align: center; - box-sizing: border-box; - cursor: pointer; - font-weight: 500; - outline: 0; - margin-bottom: 10px; - - &:hover { - background-color: lighten($color4, 5%); - } - - &:active, &:focus { - position: relative; - top: 1px; - background-color: darken($color4, 5%); - } - - &.negative { - background: $color6; - - &:hover { - background-color: lighten($color6, 5%); - } - - &:active, &:focus { - background-color: darken($color6, 5%); - } - } - } - - select { - font-size: 16px; - } -} - -.flash-message { - background: $color1; - color: $color3; - border-radius: 4px; - padding: 15px 10px; - margin-bottom: 30px; - box-shadow: 0 0 5px rgba($color8, 0.2); - text-align: center; - - strong { - font-weight: 500; - } -} - -.form-footer { - margin-top: 30px; - text-align: center; - - a { - color: $color5; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.oauth-prompt, .follow-prompt { - margin-bottom: 30px; - text-align: center; - color: $color3; - - h2 { - font-size: 16px; - margin-bottom: 30px; - } - - strong { - color: $color2; - font-weight: 500; - } -} - -.qr-wrapper { - display: flex; -} - -.qr-code { - flex: 0 0 auto; - background: #fff; - padding: 4px; - margin-bottom: 20px; - box-shadow: 0 0 15px rgba($color8, 0.2); - display: inline-block; - - svg { - display: block; - margin: 0; - } -} - -.qr-alternative { - margin-left: 10px; - color: $color3; - - samp { - display: block; - font-size: 14px; - } -} - -.table-form { - p { - max-width: 400px; - margin-bottom: 15px; - - strong { - font-weight: 500; - } - } - - .warning { - max-width: 400px; - box-sizing: border-box; - background: rgba($color6, 0.5); - color: $color5; - text-shadow: 1px 1px 0 rgba($color8, 0.3); - box-shadow: 0 2px 6px rgba($color8, 0.4); - border-radius: 4px; - padding: 10px; - margin-bottom: 15px; - - a { - color: $color5; - text-decoration: underline; - - &:hover, &:focus, &:active { - text-decoration: none; - } - } - - strong { - font-weight: 600; - display: block; - margin-bottom: 5px; - - .fa { - font-weight: 400; - } - } - } -} - -.action-pagination { - display: flex; - align-items: center; - - .actions, .pagination { - flex: 1 1 auto; - } - - .actions { - padding: 30px 0; - padding-right: 20px; - flex: 0 0 auto; - } -} - -.user_allowed_languages { - li { - float: left; - width: 50%; - } -} diff --git a/app/assets/stylesheets/landing_strip.scss b/app/assets/stylesheets/landing_strip.scss @@ -1,17 +0,0 @@ -.landing-strip { - background: rgba(darken($color1, 7%), 0.8); - color: $color3; - font-weight: 400; - padding: 14px; - border-radius: 4px; - margin-bottom: 20px; - - strong, a { - font-weight: 500; - } - - a { - color: inherit; - text-decoration: underline; - } -} diff --git a/app/assets/stylesheets/lists.scss b/app/assets/stylesheets/lists.scss @@ -1,20 +0,0 @@ -.no-list { - list-style: none; - - li { - display: inline-block; - margin: 0 5px; - } -} - -.recovery-codes { - list-style: none; - margin: 0 auto; - text-align: center; - - li { - font-size: 125%; - line-height: 1.5; - letter-spacing: 1px; - } -} diff --git a/app/assets/stylesheets/reset.scss b/app/assets/stylesheets/reset.scss @@ -1,91 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} - -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} - -body { - line-height: 1; -} - -ol, ul { - list-style: none; -} - -blockquote, q { - quotes: none; -} - -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-thumb { - background: lighten($color1, 4%); - border: 0px none $color5; - border-radius: 50px; -} - -::-webkit-scrollbar-thumb:hover { - background: lighten($color1, 6%); -} - -::-webkit-scrollbar-thumb:active { - background: lighten($color1, 4%); -} - -::-webkit-scrollbar-track { - border: 0px none $color5; - border-radius: 0; - background: rgba($color8, 0.1); -} - -::-webkit-scrollbar-track:hover { - background: $color1; -} - -::-webkit-scrollbar-track:active { - background: $color1; -} - -::-webkit-scrollbar-corner { - background: transparent; -} diff --git a/app/assets/stylesheets/rtl.scss b/app/assets/stylesheets/rtl.scss @@ -1,136 +0,0 @@ -body.rtl { - direction: rtl; - - .column-link__icon, .column-header__icon { - margin-right: 0; - margin-left: 5px; - } - - .character-counter__wrapper { - margin-right: 0; - margin-left: 16px; - } - - .navigation-bar__profile { - margin-left: 0; - margin-right: 8px; - } - - .search__input { - padding-right: 10px; - padding-left: 30px; - } - - .search__icon .fa { - right: auto; - left: 10px; - } - - .column-icon-clear { - right: auto; - left: 48px; - } - - .column-icon { - right: auto; - left: 5px; - } - - .setting-toggle { - margin-left: 0; - margin-right: 8px; - } - - .status__avatar { - left: auto; - right: 10px; - } - - .status { - padding-left: 10px; - padding-right: 68px; - } - - .status__info .status__display-name { - padding-left: 25px; - padding-right: 0; - } - - .column-back-button--slim-button { - right: auto; - left: 0; - } - - .status__info-time { - float: left; - } - - .status__action-bar-button-wrapper { - float: right; - margin-right: 0; - margin-left: 18px; - } - - .status__action-bar-dropdown { - float: right; - } - - .privacy-dropdown__dropdown { - left: auto; - right: 0; - } - - .dropdown--active .dropdown__content { - text-align: right; - } - - .dropdown--active .dropdown__content::before { - left: auto; - right: 8px; - } - - .dropdown--active .dropdown__content > ul { - left: auto; - right: -10px; - } - - .privacy-dropdown__option__icon { - margin-left: 10px; - margin-right: 0; - } - - .detailed-status__display-avatar { - margin-right: 0; - margin-left: 10px; - float: right; - } - - .detailed-status__favorites, .detailed-status__reblogs { - margin-left: 0; - margin-right: 6px; - } - - @media screen and (min-width: 1025px) { - .column, .drawer { - padding-left: 5px; - padding-right: 5px; - - &:first-child { - padding-left: 5px; - padding-right: 10px; - } - - &:last-child { - padding-right: 0px; - padding-left: 10px; - } - } - - .columns-area > div { - .column, .drawer { - padding-left: 5px; - padding-right: 5px; - } - } - } -} diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss @@ -1,372 +0,0 @@ -.activity-stream { - clear: both; - box-shadow: 0 0 15px rgba($color8, 0.2); - - .entry { - background: $color5; - - .detailed-status.light, .status.light { - border-bottom: 1px solid $color2; - } - - &:last-child { - &, .detailed-status.light, .status.light { - border-bottom: 0; - border-radius: 0 0 4px 4px; - } - } - - &:first-child { - &, .detailed-status.light, .status.light { - border-radius: 4px 4px 0 0; - } - - &:last-child { - &, .detailed-status.light, .status.light { - border-radius: 4px; - } - } - } - } - - .status.light { - padding: 14px 14px 14px (48px + 14px*2); - position: relative; - min-height: 48px; - cursor: default; - - .status__header { - font-size: 15px; - - .status__meta { - float: right; - font-size: 14px; - - .status__relative-time { - color: $color4; - } - } - } - - .status__display-name { - display: block; - max-width: 100%; - padding-right: 25px; - color: $color1; - } - - .status__avatar { - position: absolute; - left: 14px; - top: 14px; - width: 48px; - height: 48px; - - & > div { - width: 48px; - height: 48px; - } - - img { - display: block; - border-radius: 4px; - } - } - - .display-name { - display: block; - max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - strong { - font-weight: 500; - color: $color1; - } - - span { - font-size: 14px; - color: $color4; - } - } - - .status__content { - color: $color1; - - a { - color: $color4; - } - - a.status__content__spoiler-link { - color: $color5; - background: $color3; - - &:hover { - background: lighten($color3, 8%); - } - } - } - - .status__attachments { - margin-top: 8px; - overflow: hidden; - width: 100%; - box-sizing: border-box; - position: relative; - - .status__attachments__inner { - display: flex; - height: 214px; - } - } - } - - .detailed-status.light { - padding: 14px; - background: $color5; - cursor: default; - - .detailed-status__display-name { - display: block; - overflow: hidden; - margin-bottom: 15px; - - & > div { - float: left; - margin-right: 10px; - } - - .display-name { - display: block; - max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - strong { - font-weight: 500; - color: $color1; - } - - span { - font-size: 14px; - color: $color3; - } - } - } - - .avatar { - width: 48px; - height: 48px; - - img { - display: block; - border-radius: 4px; - } - } - - .status__content { - color: $color1; - - a { - color: $color4; - } - - a.status__content__spoiler-link { - color: $color5; - background: $color3; - - &:hover { - background: lighten($color3, 8%); - } - } - } - - .detailed-status__meta { - margin-top: 15px; - color: $color3; - font-size: 14px; - line-height: 18px; - - a { - color: inherit; - } - - span > span { - font-weight: 500; - font-size: 12px; - margin-left: 6px; - display: inline-block; - } - } - - .detailed-status__attachments { - margin-top: 8px; - overflow: hidden; - width: 100%; - box-sizing: border-box; - position: relative; - - .status__attachments__inner { - display: flex; - height: 360px; - } - } - - .video-player { - margin-top: 8px; - height: 300px; - overflow: hidden; - position: relative; - - video { - position: relative; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - top: 50%; - transform: translateY(-50%); - } - } - } - - .media-item, .video-item { - box-sizing: border-box; - position: relative; - left: auto; - top: auto; - right: auto; - bottom: auto; - float: left; - border: medium none; - display: block; - flex: 1 1 auto; - height: 100%; - margin-right: 2px; - - &:last-child { - margin-right: 0; - } - - a { - display: block; - width: 100%; - height: 100%; - background: no-repeat scroll center center / cover; - text-decoration: none; - cursor: zoom-in; - } - - video { - position: relative; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - top: 50%; - transform: translateY(-50%); - } - } - - .video-item { - a { - cursor: pointer; - } - - .video-item__play { - position: absolute; - top: 50%; - left: 50%; - font-size: 36px; - transform: translate(-50%, -50%); - padding: 5px; - border-radius: 100px; - color: rgba($color5, 0.8); - z-index: 1; - } - } - - .media-spoiler { - background: $color3; - width: 100%; - height: 100%; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - text-align: center; - transition: all 100ms linear; - z-index: 2; - - &:hover { - background: darken($color3, 5%); - } - - span { - display: block; - - &:first-child { - font-size: 14px; - } - - &:last-child { - font-size: 11px; - font-weight: 500; - } - } - } - - .pre-header { - padding: 14px 0px; - padding-left: (48px + 14px*2); - padding-bottom: 0; - margin-bottom: -4px; - color: $color3; - font-size: 14px; - position: relative; - - .pre-header__icon { - position: absolute; - left: (48px + 14px*2 - 30px); - } - - .status__display-name.muted strong { - color: $color3; - } - } - - .open-in-web-link { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.embed { - .activity-stream { - border-radius: 4px; - box-shadow: none; - - .entry { - &:last-child { - border-radius: 0 0 4px 4px; - } - - &:first-child { - border-radius: 4px 4px 0 0; - - &:last-child { - border-radius: 4px; - } - } - } - } -} diff --git a/app/assets/stylesheets/tables.scss b/app/assets/stylesheets/tables.scss @@ -1,65 +0,0 @@ -.table { - width: 100%; - max-width: 100%; - border-spacing: 0; - border-collapse: collapse; - margin-bottom: 20px; - - th, td { - padding: 8px; - line-height: 18px; - vertical-align: top; - border-top: 1px solid $color1; - text-align: left; - } - - & > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid $color1; - border-top: 0; - font-weight: 500; - } - - & > tbody > tr > th { - font-weight: 500; - } - - & > tbody > tr:nth-child(odd) > td, & > tbody > tr:nth-child(odd) > th { - background: $color1; - } - - a { - color: $color4; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } - - strong { - font-weight: 500; - } -} - -samp { - font-family: 'Roboto Mono', monospace; -} - -a.table-action-link { - text-decoration: none; - display: inline-block; - margin-right: 5px; - padding: 0 10px; - color: rgba($color5, 0.7); - font-weight: 500; - - &:hover { - color: $color5; - } - - i.fa { - font-weight: 400; - margin-right: 5px; - } -} diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss @@ -1,8 +0,0 @@ -$color1: #282c37 !default; // darkest -$color2: #d9e1e8 !default; // lightest -$color3: #9baec8 !default; // lighter -$color4: #2b90d9 !default; // vibrant -$color5: #ffffff !default; // white -$color6: #df405a !default; // error red -$color7: #79bd9a !default; // succ green -$color8: #000000 !default; // black diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb @@ -10,7 +10,7 @@ module ApplicationHelper end def add_rtl_body_class(other_classes) - other_classes = "#{other_classes} rtl" if [:ar, :fa].include?(I18n.locale) + other_classes = "#{other_classes} rtl" if [:ar, :fa, :he].include?(I18n.locale) other_classes end @@ -22,4 +22,8 @@ module ApplicationHelper def title Rails.env.production? ? site_title : "#{site_title} (Dev)" end + + def fa_icon(icon) + content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' ')) + end end diff --git a/app/assets/fonts/montserrat/Montserrat-Regular.eot b/app/javascript/fonts/montserrat/Montserrat-Regular.eot Binary files differ. diff --git a/app/assets/fonts/montserrat/Montserrat-Regular.ttf b/app/javascript/fonts/montserrat/Montserrat-Regular.ttf Binary files differ. diff --git a/app/assets/fonts/montserrat/Montserrat-Regular.woff b/app/javascript/fonts/montserrat/Montserrat-Regular.woff Binary files differ. diff --git a/app/assets/fonts/montserrat/Montserrat-Regular.woff2 b/app/javascript/fonts/montserrat/Montserrat-Regular.woff2 Binary files differ. diff --git a/app/assets/fonts/roboto-mono/robotomono-regular-webfont.eot b/app/javascript/fonts/roboto-mono/robotomono-regular-webfont.eot Binary files differ. diff --git a/app/assets/fonts/roboto-mono/robotomono-regular-webfont.svg b/app/javascript/fonts/roboto-mono/robotomono-regular-webfont.svg diff --git a/app/assets/fonts/roboto-mono/robotomono-regular-webfont.ttf b/app/javascript/fonts/roboto-mono/robotomono-regular-webfont.ttf Binary files differ. diff --git a/app/assets/fonts/roboto-mono/robotomono-regular-webfont.woff b/app/javascript/fonts/roboto-mono/robotomono-regular-webfont.woff Binary files differ. diff --git a/app/assets/fonts/roboto-mono/robotomono-regular-webfont.woff2 b/app/javascript/fonts/roboto-mono/robotomono-regular-webfont.woff2 Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-bold-webfont.eot b/app/javascript/fonts/roboto/roboto-bold-webfont.eot Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-bold-webfont.svg b/app/javascript/fonts/roboto/roboto-bold-webfont.svg diff --git a/app/assets/fonts/roboto/roboto-bold-webfont.ttf b/app/javascript/fonts/roboto/roboto-bold-webfont.ttf Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-bold-webfont.woff b/app/javascript/fonts/roboto/roboto-bold-webfont.woff Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-bold-webfont.woff2 b/app/javascript/fonts/roboto/roboto-bold-webfont.woff2 Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-italic-webfont.eot b/app/javascript/fonts/roboto/roboto-italic-webfont.eot Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-italic-webfont.svg b/app/javascript/fonts/roboto/roboto-italic-webfont.svg diff --git a/app/assets/fonts/roboto/roboto-italic-webfont.ttf b/app/javascript/fonts/roboto/roboto-italic-webfont.ttf Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-italic-webfont.woff b/app/javascript/fonts/roboto/roboto-italic-webfont.woff Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-italic-webfont.woff2 b/app/javascript/fonts/roboto/roboto-italic-webfont.woff2 Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-medium-webfont.eot b/app/javascript/fonts/roboto/roboto-medium-webfont.eot Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-medium-webfont.svg b/app/javascript/fonts/roboto/roboto-medium-webfont.svg diff --git a/app/assets/fonts/roboto/roboto-medium-webfont.ttf b/app/javascript/fonts/roboto/roboto-medium-webfont.ttf Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-medium-webfont.woff b/app/javascript/fonts/roboto/roboto-medium-webfont.woff Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-medium-webfont.woff2 b/app/javascript/fonts/roboto/roboto-medium-webfont.woff2 Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-regular-webfont.eot b/app/javascript/fonts/roboto/roboto-regular-webfont.eot Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-regular-webfont.svg b/app/javascript/fonts/roboto/roboto-regular-webfont.svg diff --git a/app/assets/fonts/roboto/roboto-regular-webfont.ttf b/app/javascript/fonts/roboto/roboto-regular-webfont.ttf Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-regular-webfont.woff b/app/javascript/fonts/roboto/roboto-regular-webfont.woff Binary files differ. diff --git a/app/assets/fonts/roboto/roboto-regular-webfont.woff2 b/app/javascript/fonts/roboto/roboto-regular-webfont.woff2 Binary files differ. diff --git a/app/assets/javascripts/components/.gitkeep b/app/javascript/images/.keep diff --git a/app/assets/images/background-photo.jpg b/app/javascript/images/background-photo.jpg Binary files differ. diff --git a/app/assets/images/boost_sprite.png b/app/javascript/images/boost_sprite.png Binary files differ. diff --git a/app/assets/images/elephant-friend.png b/app/javascript/images/elephant-friend.png Binary files differ. diff --git a/app/assets/images/fluffy-elephant-friend.png b/app/javascript/images/fluffy-elephant-friend.png Binary files differ. diff --git a/app/assets/images/logo.png b/app/javascript/images/logo.png Binary files differ. diff --git a/app/assets/images/logo.svg b/app/javascript/images/logo.svg diff --git a/app/assets/images/mastodon-getting-started.png b/app/javascript/images/mastodon-getting-started.png Binary files differ. diff --git a/app/assets/images/mastodon-not-found.png b/app/javascript/images/mastodon-not-found.png Binary files differ. diff --git a/app/assets/images/mastodon.jpg b/app/javascript/images/mastodon.jpg Binary files differ. diff --git a/app/assets/images/mastodon_small.jpg b/app/javascript/images/mastodon_small.jpg Binary files differ. diff --git a/app/assets/images/screenshot.png b/app/javascript/images/screenshot.png Binary files differ. diff --git a/app/assets/images/void.png b/app/javascript/images/void.png Binary files differ. diff --git a/app/assets/images/.keep b/app/javascript/mastodon/.gitkeep diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/javascript/mastodon/actions/accounts.js diff --git a/app/assets/javascripts/components/actions/alerts.jsx b/app/javascript/mastodon/actions/alerts.js diff --git a/app/assets/javascripts/components/actions/blocks.jsx b/app/javascript/mastodon/actions/blocks.js diff --git a/app/assets/javascripts/components/actions/cards.jsx b/app/javascript/mastodon/actions/cards.js diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/javascript/mastodon/actions/compose.js diff --git a/app/assets/javascripts/components/actions/favourites.jsx b/app/javascript/mastodon/actions/favourites.js diff --git a/app/assets/javascripts/components/actions/interactions.jsx b/app/javascript/mastodon/actions/interactions.js diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/javascript/mastodon/actions/modal.js diff --git a/app/assets/javascripts/components/actions/mutes.jsx b/app/javascript/mastodon/actions/mutes.js diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/javascript/mastodon/actions/notifications.js diff --git a/app/assets/javascripts/components/actions/onboarding.jsx b/app/javascript/mastodon/actions/onboarding.js diff --git a/app/assets/javascripts/components/actions/reports.jsx b/app/javascript/mastodon/actions/reports.js diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/javascript/mastodon/actions/search.js diff --git a/app/assets/javascripts/components/actions/settings.jsx b/app/javascript/mastodon/actions/settings.js diff --git a/app/assets/javascripts/components/actions/statuses.jsx b/app/javascript/mastodon/actions/statuses.js diff --git a/app/assets/javascripts/components/actions/store.jsx b/app/javascript/mastodon/actions/store.js diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/javascript/mastodon/actions/timelines.js diff --git a/app/assets/javascripts/components/api.jsx b/app/javascript/mastodon/api.js diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js @@ -0,0 +1,93 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from './avatar'; +import DisplayName from './display_name'; +import Permalink from './permalink'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +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 @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' } +}); + +class Account extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleFollow = this.handleFollow.bind(this); + this.handleBlock = this.handleBlock.bind(this); + this.handleMute = this.handleMute.bind(this); + } + + handleFollow () { + this.props.onFollow(this.props.account); + } + + handleBlock () { + this.props.onBlock(this.props.account); + } + + handleMute () { + this.props.onMute(this.props.account); + } + + render () { + const { account, me, intl } = this.props; + + if (!account) { + return <div />; + } + + let buttons; + + if (account.get('id') !== me && account.get('relationship', null) !== null) { + 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 = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> + } else if (blocking) { + buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; + } else if (muting) { + buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; + } else { + buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; + } + } + + return ( + <div className='account'> + <div className='account__wrapper'> + <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div> + <DisplayName account={account} /> + </Permalink> + + <div className='account__relationship'> + {buttons} + </div> + </div> + </div> + ); + } + +} + +Account.propTypes = { + account: ImmutablePropTypes.map.isRequired, + me: PropTypes.number.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +} + +export default injectIntl(Account); diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js @@ -0,0 +1,33 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; + +class AttachmentList extends React.PureComponent { + + render () { + const { media } = this.props; + + return ( + <div className='attachment-list'> + <div className='attachment-list__icon'> + <i className='fa fa-link' /> + </div> + + <ul className='attachment-list__list'> + {media.map(attachment => + <li key={attachment.get('id')}> + <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a> + </li> + )} + </ul> + </div> + ); + } +} + +AttachmentList.propTypes = { + media: ImmutablePropTypes.list.isRequired +}; + +export default AttachmentList; diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -0,0 +1,213 @@ +import React from 'react'; +import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { isRtl } from '../rtl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const textAtCursorMatchesToken = (str, caretPosition) => { + let word; + + let left = str.slice(0, caretPosition).search(/\S+$/); + let right = str.slice(caretPosition).search(/\s/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 2 || word[0] !== '@') { + return [null, null]; + } + + word = word.trim().toLowerCase().slice(1); + + if (word.length > 0) { + return [left + 1, word]; + } else { + return [null, null]; + } +}; + +class AutosuggestTextarea extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + suggestionsHidden: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0 + }; + this.onChange = this.onChange.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onSuggestionClick = this.onSuggestionClick.bind(this); + this.setTextarea = this.setTextarea.bind(this); + this.onPaste = this.onPaste.bind(this); + } + + onChange (e) { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + // auto-resize textarea + e.target.style.height = `${e.target.scrollHeight}px`; + + this.props.onChange(e); + } + + onKeyDown (e) { + const { suggestions, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + + if (disabled) { + e.preventDefault(); + return; + } + + switch(e.key) { + case 'Escape': + if (!suggestionsHidden) { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); + } + + break; + case 'ArrowUp': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + e.stopPropagation(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + this.props.onKeyDown(e); + } + + onBlur () { + // If we hide the suggestions immediately, then this will prevent the + // onClick for the suggestions themselves from firing. + // Setting a short window for that to take place before hiding the + // suggestions ensures that can't happen. + setTimeout(() => { + this.setState({ suggestionsHidden: true }); + }, 100); + } + + onSuggestionClick (suggestion, e) { + e.preventDefault(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.textarea.focus(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { + this.setState({ suggestionsHidden: false }); + } + } + + setTextarea (c) { + this.textarea = c; + } + + onPaste (e) { + if (e.clipboardData && e.clipboardData.files.length === 1) { + this.props.onPaste(e.clipboardData.files) + e.preventDefault(); + } + } + + reset () { + this.textarea.style.height = 'auto'; + } + + render () { + const { value, suggestions, disabled, placeholder, onKeyUp } = this.props; + const { suggestionsHidden, selectedSuggestion } = this.state; + const style = { direction: 'ltr' }; + + if (isRtl(value)) { + style.direction = 'rtl'; + } + + return ( + <div className='autosuggest-textarea'> + <textarea + ref={this.setTextarea} + className='autosuggest-textarea__textarea' + disabled={disabled} + placeholder={placeholder} + autoFocus={true} + value={value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyUp={onKeyUp} + onBlur={this.onBlur} + onPaste={this.onPaste} + style={style} + /> + + <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> + {suggestions.map((suggestion, i) => ( + <div + role='button' + tabIndex='0' + key={suggestion} + className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} + onClick={this.onSuggestionClick.bind(this, suggestion)}> + <AutosuggestAccountContainer id={suggestion} /> + </div> + ))} + </div> + </div> + ); + } + +}; + +AutosuggestTextarea.propTypes = { + value: PropTypes.string, + suggestions: ImmutablePropTypes.list, + disabled: PropTypes.bool, + placeholder: PropTypes.string, + onSuggestionSelected: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, + onPaste: PropTypes.func.isRequired, +}; + +export default AutosuggestTextarea; diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Avatar extends React.PureComponent { + + constructor (props, context) { + super(props, context); + + this.state = { + hovering: false + }; + + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleMouseLeave = this.handleMouseLeave.bind(this); + } + + handleMouseEnter () { + if (this.props.animate) return; + this.setState({ hovering: true }); + } + + handleMouseLeave () { + if (this.props.animate) return; + this.setState({ hovering: false }); + } + + render () { + const { src, size, staticSrc, animate } = this.props; + const { hovering } = this.state; + + const style = { + ...this.props.style, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px` + }; + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; + } + + return ( + <div + className='account__avatar' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + style={style} + /> + ); + } + +} + +Avatar.propTypes = { + src: PropTypes.string.isRequired, + staticSrc: PropTypes.string, + size: PropTypes.number.isRequired, + style: PropTypes.object, + animate: PropTypes.bool +}; + +Avatar.defaultProps = { + animate: false +}; + +export default Avatar; diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Button extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick (e) { + if (!this.props.disabled) { + this.props.onClick(); + } + } + + render () { + const style = { + display: this.props.block ? 'block' : 'inline-block', + width: this.props.block ? '100%' : 'auto', + padding: `0 ${this.props.size / 2.25}px`, + height: `${this.props.size}px`, + lineHeight: `${this.props.size}px` + }; + + return ( + <button className={`button ${this.props.secondary ? 'button-secondary' : ''}`} disabled={this.props.disabled} onClick={this.handleClick} style={{ ...style, ...this.props.style }}> + {this.props.text || this.props.children} + </button> + ); + } + +} + +Button.propTypes = { + text: PropTypes.node, + onClick: PropTypes.func, + disabled: PropTypes.bool, + block: PropTypes.bool, + secondary: PropTypes.bool, + size: PropTypes.number, + style: PropTypes.object, + children: PropTypes.node +}; + +Button.defaultProps = { + size: 36 +}; + +export default Button; diff --git a/app/javascript/mastodon/components/collapsable.js b/app/javascript/mastodon/components/collapsable.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { Motion, spring } from 'react-motion'; +import PropTypes from 'prop-types'; + +const Collapsable = ({ fullHeight, isVisible, children }) => ( + <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}> + {({ opacity, height }) => + <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}> + {children} + </div> + } + </Motion> +); + +Collapsable.propTypes = { + fullHeight: PropTypes.number.isRequired, + isVisible: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired +}; + +export default Collapsable; diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +class ColumnBackButton extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick () { + if (window.history && window.history.length === 1) this.context.router.push("/"); + else this.context.router.goBack(); + } + + render () { + return ( + <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'> + <i className='fa fa-fw fa-chevron-left column-back-button__icon'/> + <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> + </div> + ); + } + +}; + +ColumnBackButton.contextTypes = { + router: PropTypes.object +}; + +export default ColumnBackButton; diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +class ColumnBackButtonSlim extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick () { + this.context.router.push('/'); + } + + render () { + return ( + <div className='column-back-button--slim'> + <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> + <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> + </div> + </div> + ); + } +} + +ColumnBackButtonSlim.contextTypes = { + router: PropTypes.object +}; + +export default ColumnBackButtonSlim; diff --git a/app/javascript/mastodon/components/column_collapsable.js b/app/javascript/mastodon/components/column_collapsable.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Motion, spring } from 'react-motion'; +import PropTypes from 'prop-types'; + +class ColumnCollapsable extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + collapsed: true + }; + + this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this); + } + + handleToggleCollapsed () { + const currentState = this.state.collapsed; + + this.setState({ collapsed: !currentState }); + + if (!currentState && this.props.onCollapse) { + this.props.onCollapse(); + } + } + + render () { + const { icon, title, fullHeight, children } = this.props; + const { collapsed } = this.state; + const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable'; + + return ( + <div className='column-collapsable'> + <div role='button' tabIndex='0' title={`${title}`} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}> + <i className={`fa fa-${icon}`} /> + </div> + + <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> + {({ opacity, height }) => + <div style={{ overflow: height === fullHeight ? 'auto' : 'hidden', height: `${height}px`, opacity: opacity / 100, maxHeight: '70vh' }}> + {children} + </div> + } + </Motion> + </div> + ); + } +} + +ColumnCollapsable.propTypes = { + icon: PropTypes.string.isRequired, + title: PropTypes.string, + fullHeight: PropTypes.number.isRequired, + children: PropTypes.node, + onCollapse: PropTypes.func +}; + +export default ColumnCollapsable; diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js @@ -0,0 +1,25 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import escapeTextContentForBrowser from 'escape-html'; +import emojify from '../emoji'; + +class DisplayName extends React.PureComponent { + + render () { + const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name'); + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + + return ( + <span className='display-name'> + <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> + </span> + ); + } + +}; + +DisplayName.propTypes = { + account: ImmutablePropTypes.map.isRequired +} + +export default DisplayName; diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js @@ -0,0 +1,79 @@ +import React from 'react'; +import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; +import PropTypes from 'prop-types'; + +class DropdownMenu extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + direction: 'left' + }; + this.setRef = this.setRef.bind(this); + this.renderItem = this.renderItem.bind(this); + } + + setRef (c) { + this.dropdown = c; + } + + handleClick (i, e) { + const { action } = this.props.items[i]; + + if (typeof action === 'function') { + e.preventDefault(); + action(); + this.dropdown.hide(); + } + } + + renderItem (item, i) { + if (item === null) { + return <li key={ 'sep' + i } className='dropdown__sep' />; + } + + const { text, action, href = '#' } = item; + + return ( + <li className='dropdown__content-list-item' key={ text + i }> + <a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'> + {text} + </a> + </li> + ); + } + + render () { + const { icon, items, size, direction, ariaLabel } = this.props; + const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right"; + + return ( + <Dropdown ref={this.setRef}> + <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}> + <i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} /> + </DropdownTrigger> + + <DropdownContent className={directionClass}> + <ul className='dropdown__content-list'> + {items.map(this.renderItem)} + </ul> + </DropdownContent> + </Dropdown> + ); + } + +} + +DropdownMenu.propTypes = { + icon: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + size: PropTypes.number.isRequired, + direction: PropTypes.string, + ariaLabel: PropTypes.string +}; + +DropdownMenu.defaultProps = { + ariaLabel: "Menu" +}; + +export default DropdownMenu; diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class ExtendedVideoPlayer extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleLoadedData = this.handleLoadedData.bind(this); + this.setRef = this.setRef.bind(this); + } + + handleLoadedData () { + if (this.props.time) { + this.video.currentTime = this.props.time; + } + } + + componentDidMount () { + this.video.addEventListener('loadeddata', this.handleLoadedData); + } + + componentWillUnmount () { + this.video.removeEventListener('loadeddata', this.handleLoadedData); + } + + setRef (c) { + this.video = c; + } + + render () { + return ( + <div className='extended-video-player'> + <video + ref={this.setRef} + src={this.props.src} + autoPlay + muted={this.props.muted} + controls={this.props.controls} + loop={!this.props.controls} + /> + </div> + ); + } + +} + +ExtendedVideoPlayer.propTypes = { + src: PropTypes.string.isRequired, + time: PropTypes.number, + controls: PropTypes.bool.isRequired, + muted: PropTypes.bool.isRequired +}; + +export default ExtendedVideoPlayer; diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { Motion, spring } from 'react-motion'; +import PropTypes from 'prop-types'; + +class IconButton extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick (e) { + e.preventDefault(); + + if (!this.props.disabled) { + this.props.onClick(e); + } + } + + render () { + let style = { + fontSize: `${this.props.size}px`, + width: `${this.props.size * 1.28571429}px`, + height: `${this.props.size * 1.28571429}px`, + lineHeight: `${this.props.size}px`, + ...this.props.style + }; + + if (this.props.active) { + style = { ...style, ...this.props.activeStyle }; + } + + const classes = ['icon-button']; + + if (this.props.active) { + classes.push('active'); + } + + if (this.props.disabled) { + classes.push('disabled'); + } + + if (this.props.inverted) { + classes.push('inverted'); + } + + if (this.props.overlay) { + classes.push('overlayed'); + } + + if (this.props.className) { + classes.push(this.props.className) + } + + return ( + <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> + {({ rotate }) => + <button + aria-label={this.props.title} + title={this.props.title} + className={classes.join(' ')} + onClick={this.handleClick} + style={style}> + <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> + </button> + } + </Motion> + ); + } + +} + +IconButton.propTypes = { + className: PropTypes.string, + title: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + onClick: PropTypes.func, + size: PropTypes.number, + active: PropTypes.bool, + style: PropTypes.object, + activeStyle: PropTypes.object, + disabled: PropTypes.bool, + inverted: PropTypes.bool, + animate: PropTypes.bool, + overlay: PropTypes.bool +}; + +IconButton.defaultProps = { + size: 18, + active: false, + disabled: false, + animate: false, + overlay: false +}; + +export default IconButton; diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +const LoadMore = ({ onClick }) => ( + <a href="#" className='load-more' role='button' onClick={onClick}> + <FormattedMessage id='status.load_more' defaultMessage='Load more' /> + </a> +); + +LoadMore.propTypes = { + onClick: PropTypes.func +}; + +export default LoadMore; diff --git a/app/javascript/mastodon/components/loading_indicator.js b/app/javascript/mastodon/components/loading_indicator.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +const LoadingIndicator = () => ( + <div className='loading-indicator'> + <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> + </div> +); + +export default LoadingIndicator; diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js @@ -0,0 +1,196 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { isIOS } from '../is_mobile'; + +const messages = defineMessages({ + toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' } +}); + +class Item extends React.PureComponent { + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick (e) { + const { index, onClick } = this.props; + + if (e.button === 0) { + e.preventDefault(); + onClick(index); + } + + e.stopPropagation(); + } + + render () { + const { attachment, index, size } = this.props; + + let width = 50; + let height = 100; + let top = 'auto'; + let left = 'auto'; + let bottom = 'auto'; + let right = 'auto'; + + if (size === 1) { + width = 100; + } + + if (size === 4 || (size === 3 && index > 0)) { + height = 50; + } + + if (size === 2) { + if (index === 0) { + right = '2px'; + } else { + left = '2px'; + } + } else if (size === 3) { + if (index === 0) { + right = '2px'; + } else if (index > 0) { + left = '2px'; + } + + if (index === 1) { + bottom = '2px'; + } else if (index > 1) { + top = '2px'; + } + } else if (size === 4) { + if (index === 0 || index === 2) { + right = '2px'; + } + + if (index === 1 || index === 3) { + left = '2px'; + } + + if (index < 2) { + bottom = '2px'; + } else { + top = '2px'; + } + } + + let thumbnail = ''; + + if (attachment.get('type') === 'image') { + thumbnail = ( + <a + className='media-gallery__item-thumbnail' + href={attachment.get('remote_url') || attachment.get('url')} + onClick={this.handleClick} + target='_blank' + style={{ backgroundImage: `url(${attachment.get('preview_url')})` }} + /> + ); + } else if (attachment.get('type') === 'gifv') { + const autoPlay = !isIOS() && this.props.autoPlayGif; + + thumbnail = ( + <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}> + <video + className='media-gallery__item-gifv-thumbnail' + role='application' + src={attachment.get('url')} + onClick={this.handleClick} + autoPlay={autoPlay} + loop={true} + muted={true} + /> + + <span className='media-gallery__gifv__label'>GIF</span> + </div> + ); + } + + return ( + <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + {thumbnail} + </div> + ); + } + +} + +Item.propTypes = { + attachment: ImmutablePropTypes.map.isRequired, + index: PropTypes.number.isRequired, + size: PropTypes.number.isRequired, + onClick: PropTypes.func.isRequired, + autoPlayGif: PropTypes.bool.isRequired +}; + +class MediaGallery extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + visible: !props.sensitive + }; + this.handleOpen = this.handleOpen.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + handleOpen (e) { + this.setState({ visible: !this.state.visible }); + } + + handleClick (index) { + this.props.onOpenMedia(this.props.media, index); + } + + render () { + const { media, intl, sensitive } = this.props; + + let children; + + if (!this.state.visible) { + let warning; + + if (sensitive) { + warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; + } else { + warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; + } + + children = ( + <div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}> + <span className='media-spoiler__warning'>{warning}</span> + <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> + </div> + ); + } else { + const size = media.take(4).size; + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />); + } + + return ( + <div className='media-gallery' style={{ height: `${this.props.height}px` }}> + <div className='spoiler-button' style={{ display: !this.state.visible ? 'none' : 'block' }}> + <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> + </div> + + {children} + </div> + ); + } + +} + +MediaGallery.propTypes = { + sensitive: PropTypes.bool, + media: ImmutablePropTypes.list.isRequired, + height: PropTypes.number.isRequired, + onOpenMedia: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + autoPlayGif: PropTypes.bool.isRequired +}; + +export default injectIntl(MediaGallery); diff --git a/app/javascript/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +const MissingIndicator = () => ( + <div className='missing-indicator'> + <div> + <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> + </div> + </div> +); + +export default MissingIndicator; diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Permalink extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick (e) { + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(this.props.to); + } + } + + render () { + const { href, children, className, ...other } = this.props; + + return ( + <a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}> + {children} + </a> + ); + } + +} + +Permalink.contextTypes = { + router: PropTypes.object +}; + +Permalink.propTypes = { + className: PropTypes.string, + href: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + children: PropTypes.node +}; + +export default Permalink; diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { injectIntl, FormattedRelative } from 'react-intl'; +import PropTypes from 'prop-types'; + +const RelativeTimestamp = ({ intl, timestamp }) => { + const date = new Date(timestamp); + + return ( + <time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}> + <FormattedRelative value={date} /> + </time> + ); +}; + +RelativeTimestamp.propTypes = { + intl: PropTypes.object.isRequired, + timestamp: PropTypes.string.isRequired +}; + +export default injectIntl(RelativeTimestamp); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js @@ -0,0 +1,123 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from './avatar'; +import RelativeTimestamp from './relative_timestamp'; +import DisplayName from './display_name'; +import MediaGallery from './media_gallery'; +import VideoPlayer from './video_player'; +import AttachmentList from './attachment_list'; +import StatusContent from './status_content'; +import StatusActionBar from './status_action_bar'; +import { FormattedMessage } from 'react-intl'; +import emojify from '../emoji'; +import escapeTextContentForBrowser from 'escape-html'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class Status extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + this.handleAccountClick = this.handleAccountClick.bind(this); + } + + handleClick () { + const { status } = this.props; + this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); + } + + handleAccountClick (id, e) { + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/accounts/${id}`); + } + } + + render () { + let media = ''; + const { status, ...other } = this.props; + + if (status === null) { + return <div />; + } + + if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { + let displayName = status.getIn(['account', 'display_name']); + + if (displayName.length === 0) { + displayName = status.getIn(['account', 'username']); + } + + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + + return ( + <div className='status__wrapper'> + <div className='status__prepend'> + <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> + <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> + </div> + + <Status {...other} wrapped={true} status={status.get('reblog')} /> + </div> + ); + } + + if (status.get('media_attachments').size > 0 && !this.props.muted) { + if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { + + } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />; + } else { + media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />; + } + } + + return ( + <div className={this.props.muted ? 'status muted' : 'status'}> + <div className='status__info'> + <div className='status__info-time'> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + </div> + + <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'> + <div className='status__avatar'> + <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> + </div> + + <DisplayName account={status.get('account')} /> + </a> + </div> + + <StatusContent status={status} onClick={this.handleClick} /> + + {media} + + <StatusActionBar {...this.props} /> + </div> + ); + } + +} + +Status.contextTypes = { + router: PropTypes.object +}; + +Status.propTypes = { + status: ImmutablePropTypes.map, + wrapped: PropTypes.bool, + onReply: PropTypes.func, + onFavourite: PropTypes.func, + onReblog: PropTypes.func, + onDelete: PropTypes.func, + onOpenMedia: PropTypes.func, + onOpenVideo: PropTypes.func, + onBlock: PropTypes.func, + me: PropTypes.number, + boostModal: PropTypes.bool, + autoPlayGif: PropTypes.bool, + muted: PropTypes.bool +}; + +export default Status; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js @@ -0,0 +1,138 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from './icon_button'; +import DropdownMenu from './dropdown_menu'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + open: { id: 'status.open', defaultMessage: 'Expand this status' }, + report: { id: 'status.report', defaultMessage: 'Report @{name}' } +}); + +class StatusActionBar extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleReplyClick = this.handleReplyClick.bind(this); + this.handleFavouriteClick = this.handleFavouriteClick.bind(this); + this.handleReblogClick = this.handleReblogClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handleMentionClick = this.handleMentionClick.bind(this); + this.handleMuteClick = this.handleMuteClick.bind(this); + this.handleBlockClick = this.handleBlockClick.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleReport = this.handleReport.bind(this); + } + + handleReplyClick () { + this.props.onReply(this.props.status, this.context.router); + } + + handleFavouriteClick () { + this.props.onFavourite(this.props.status); + } + + handleReblogClick (e) { + this.props.onReblog(this.props.status, e); + } + + handleDeleteClick () { + this.props.onDelete(this.props.status); + } + + handleMentionClick () { + this.props.onMention(this.props.status.get('account'), this.context.router); + } + + handleMuteClick () { + this.props.onMute(this.props.status.get('account')); + } + + handleBlockClick () { + this.props.onBlock(this.props.status.get('account')); + } + + handleOpen () { + this.context.router.push(`/statuses/${this.props.status.get('id')}`); + } + + handleReport () { + this.props.onReport(this.props.status); + this.context.router.push('/report'); + } + + render () { + const { status, me, intl } = this.props; + const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; + let menu = []; + + menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); + menu.push(null); + + if (status.getIn(['account', 'id']) === me) { + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); + menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); + 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'; + let reply_icon; + let reply_title; + if (status.get('in_reply_to_id', null) === null) { + reply_icon = "reply"; + reply_title = intl.formatMessage(messages.reply); + } else { + reply_icon = "reply-all"; + reply_title = intl.formatMessage(messages.replyAll); + } + + return ( + <div className='status__action-bar'> + <div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div> + <div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> + <div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div> + + <div className='status__action-bar-dropdown'> + <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/> + </div> + </div> + ); + } + +} + +StatusActionBar.contextTypes = { + router: PropTypes.object +}; + +StatusActionBar.propTypes = { + status: ImmutablePropTypes.map.isRequired, + onReply: PropTypes.func, + onFavourite: PropTypes.func, + onReblog: PropTypes.func, + onDelete: PropTypes.func, + onMention: PropTypes.func, + onMute: PropTypes.func, + onBlock: PropTypes.func, + onReport: PropTypes.func, + me: PropTypes.number.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(StatusActionBar); diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js @@ -0,0 +1,165 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import escapeTextContentForBrowser from 'escape-html'; +import PropTypes from 'prop-types'; +import emojify from '../emoji'; +import { isRtl } from '../rtl'; +import { FormattedMessage } from 'react-intl'; +import Permalink from './permalink'; + +class StatusContent extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + hidden: true + }; + this.onMentionClick = this.onMentionClick.bind(this); + this.onHashtagClick = this.onHashtagClick.bind(this); + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleSpoilerClick = this.handleSpoilerClick.bind(this); + this.setRef = this.setRef.bind(this); + }; + + componentDidMount () { + const node = this.node; + const links = node.querySelectorAll('a'); + + for (var i = 0; i < links.length; ++i) { + let link = links[i]; + let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); + let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || (item.get('remote_url').length > 0 && link.href === item.get('remote_url'))); + + if (mention) { + link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + link.setAttribute('title', mention.get('acct')); + } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { + link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); + } else if (media) { + link.innerHTML = '<i class="fa fa-fw fa-photo"></i>'; + } else { + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener'); + link.setAttribute('title', link.href); + } + } + } + + onMentionClick (mention, e) { + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/accounts/${mention.get('id')}`); + } + } + + onHashtagClick (hashtag, e) { + hashtag = hashtag.replace(/^#/, '').toLowerCase(); + + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/timelines/tag/${hashtag}`); + } + } + + handleMouseDown (e) { + this.startXY = [e.clientX, e.clientY]; + } + + handleMouseUp (e) { + const [ startX, startY ] = this.startXY; + const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; + + if (e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) { + return; + } + + if (deltaX + deltaY < 5 && e.button === 0) { + this.props.onClick(); + } + + this.startXY = null; + } + + handleSpoilerClick (e) { + e.preventDefault(); + this.setState({ hidden: !this.state.hidden }); + } + + setRef (c) { + this.node = c; + } + + render () { + const { status } = this.props; + const { hidden } = this.state; + + const content = { __html: emojify(status.get('content')) }; + const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; + const directionStyle = { direction: 'ltr' }; + + if (isRtl(status.get('content'))) { + directionStyle.direction = 'rtl'; + } + + if (status.get('spoiler_text').length > 0) { + let mentionsPlaceholder = ''; + + const mentionLinks = status.get('mentions').map(item => ( + <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'> + @<span>{item.get('username')}</span> + </Permalink> + )).reduce((aggregate, item) => [...aggregate, item, ' '], []) + + const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />; + + if (hidden) { + mentionsPlaceholder = <div>{mentionLinks}</div>; + } + + return ( + <div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} > + <span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a> + </p> + + {mentionsPlaceholder} + + <div ref={this.setRef} style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> + </div> + ); + } else if (this.props.onClick) { + return ( + <div + ref={this.setRef} + className='status__content' + style={{ ...directionStyle }} + onMouseDown={this.handleMouseDown} + onMouseUp={this.handleMouseUp} + dangerouslySetInnerHTML={content} + /> + ); + } else { + return ( + <div + ref={this.setRef} + className='status__content status__content--no-action' + style={{ ...directionStyle }} + dangerouslySetInnerHTML={content} + /> + ); + } + } + +} + +StatusContent.contextTypes = { + router: PropTypes.object +}; + +StatusContent.propTypes = { + status: ImmutablePropTypes.map.isRequired, + onClick: PropTypes.func +}; + +export default StatusContent; diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js @@ -0,0 +1,130 @@ +import React from 'react'; +import Status from './status'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { ScrollContainer } from 'react-router-scroll'; +import PropTypes from 'prop-types'; +import StatusContainer from '../containers/status_container'; +import LoadMore from './load_more'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class StatusList extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + this.setRef = this.setRef.bind(this); + this.handleLoadMore = this.handleLoadMore.bind(this); + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + const offset = scrollHeight - scrollTop - clientHeight; + this._oldScrollPosition = scrollHeight - scrollTop; + + if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) { + this.props.onScrollToBottom(); + } else if (scrollTop < 100 && this.props.onScrollToTop) { + this.props.onScrollToTop(); + } else if (this.props.onScroll) { + this.props.onScroll(); + } + } + + componentDidMount () { + this.attachScrollListener(); + } + + componentDidUpdate (prevProps) { + if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) { + this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition; + } + } + + componentWillUnmount () { + this.detachScrollListener(); + } + + attachScrollListener () { + this.node.addEventListener('scroll', this.handleScroll); + } + + detachScrollListener () { + this.node.removeEventListener('scroll', this.handleScroll); + } + + setRef (c) { + this.node = c; + } + + handleLoadMore (e) { + e.preventDefault(); + this.props.onScrollToBottom(); + } + + render () { + const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props; + + let loadMore = ''; + let scrollableArea = ''; + let unread = ''; + + if (!isLoading && statusIds.size > 0 && hasMore) { + loadMore = <LoadMore onClick={this.handleLoadMore} />; + } + + if (isUnread) { + unread = <div className='status-list__unread-indicator' />; + } + + if (isLoading || statusIds.size > 0 || !emptyMessage) { + scrollableArea = ( + <div className='scrollable' ref={this.setRef}> + {unread} + + <div className='status-list'> + {prepend} + + {statusIds.map((statusId) => { + return <StatusContainer key={statusId} id={statusId} />; + })} + + {loadMore} + </div> + </div> + ); + } else { + scrollableArea = ( + <div className='empty-column-indicator' ref={this.setRef}> + {emptyMessage} + </div> + ); + } + + return ( + <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> + {scrollableArea} + </ScrollContainer> + ); + } + +} + +StatusList.propTypes = { + scrollKey: PropTypes.string.isRequired, + statusIds: ImmutablePropTypes.list.isRequired, + onScrollToBottom: PropTypes.func, + onScrollToTop: PropTypes.func, + onScroll: PropTypes.func, + shouldUpdateScroll: PropTypes.func, + isLoading: PropTypes.bool, + isUnread: PropTypes.bool, + hasMore: PropTypes.bool, + prepend: PropTypes.node, + emptyMessage: PropTypes.node +}; + +StatusList.defaultProps = { + trackScroll: true +}; + +export default StatusList; diff --git a/app/javascript/mastodon/components/video_player.js b/app/javascript/mastodon/components/video_player.js @@ -0,0 +1,210 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { isIOS } from '../is_mobile'; + +const messages = defineMessages({ + toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, + toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }, + expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }, + expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' } +}); + +class VideoPlayer extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + visible: !this.props.sensitive, + preview: true, + muted: true, + hasAudio: true, + videoError: false + }; + + this.handleClick = this.handleClick.bind(this); + this.handleVideoClick = this.handleVideoClick.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleVisibility = this.handleVisibility.bind(this); + this.handleExpand = this.handleExpand.bind(this); + this.setRef = this.setRef.bind(this); + this.handleLoadedData = this.handleLoadedData.bind(this); + this.handleVideoError = this.handleVideoError.bind(this); + } + + handleClick () { + this.setState({ muted: !this.state.muted }); + } + + handleVideoClick (e) { + e.stopPropagation(); + + const node = this.video; + + if (node.paused) { + node.play(); + } else { + node.pause(); + } + } + + handleOpen () { + this.setState({ preview: !this.state.preview }); + } + + handleVisibility () { + this.setState({ + visible: !this.state.visible, + preview: true + }); + } + + handleExpand () { + this.video.pause(); + this.props.onOpenVideo(this.props.media, this.video.currentTime); + } + + setRef (c) { + this.video = c; + } + + handleLoadedData () { + if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) { + this.setState({ hasAudio: false }); + } + } + + handleVideoError () { + this.setState({ videoError: true }); + } + + componentDidMount () { + if (!this.video) { + return; + } + + this.video.addEventListener('loadeddata', this.handleLoadedData); + this.video.addEventListener('error', this.handleVideoError); + } + + componentDidUpdate () { + if (!this.video) { + return; + } + + this.video.addEventListener('loadeddata', this.handleLoadedData); + this.video.addEventListener('error', this.handleVideoError); + } + + componentWillUnmount () { + if (!this.video) { + return; + } + + this.video.removeEventListener('loadeddata', this.handleLoadedData); + this.video.removeEventListener('error', this.handleVideoError); + } + + render () { + const { media, intl, width, height, sensitive, autoplay } = this.props; + + let spoilerButton = ( + <div className='status__video-player-spoiler' style={{ display: !this.state.visible ? 'none' : 'block' }} > + <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> + </div> + ); + + let expandButton = ( + <div className='status__video-player-expand'> + <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} /> + </div> + ); + + let muteButton = ''; + + if (this.state.hasAudio) { + muteButton = ( + <div className='status__video-player-mute'> + <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /> + </div> + ); + } + + if (!this.state.visible) { + if (sensitive) { + return ( + <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> + {spoilerButton} + <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> + <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> + </div> + ); + } else { + return ( + <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> + {spoilerButton} + <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> + <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> + </div> + ); + } + } + + if (this.state.preview && !autoplay) { + return ( + <div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center` }} onClick={this.handleOpen}> + {spoilerButton} + <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> + </div> + ); + } + + if (this.state.videoError) { + return ( + <div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' > + <span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span> + </div> + ); + } + + return ( + <div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}> + {spoilerButton} + {muteButton} + {expandButton} + + <video + className='status__video-player-video' + role='button' + tabIndex='0' + ref={this.setRef} + src={media.get('url')} + autoPlay={!isIOS()} + loop={true} + muted={this.state.muted} + onClick={this.handleVideoClick} + /> + </div> + ); + } + +} + +VideoPlayer.propTypes = { + media: ImmutablePropTypes.map.isRequired, + width: PropTypes.number, + height: PropTypes.number, + sensitive: PropTypes.bool, + intl: PropTypes.object.isRequired, + autoplay: PropTypes.bool, + onOpenVideo: PropTypes.func.isRequired +}; + +VideoPlayer.defaultProps = { + width: 239, + height: 110 +}; + +export default injectIntl(VideoPlayer); diff --git a/app/assets/javascripts/components/containers/account_container.jsx b/app/javascript/mastodon/containers/account_container.js diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js @@ -0,0 +1,314 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import configureStore from '../store/configureStore'; +import { + refreshTimelineSuccess, + updateTimeline, + deleteFromTimelines, + refreshTimeline, + connectTimeline, + disconnectTimeline +} from '../actions/timelines'; +import { showOnboardingOnce } from '../actions/onboarding'; +import { updateNotifications, refreshNotifications } from '../actions/notifications'; +import createBrowserHistory from 'history/lib/createBrowserHistory'; +import { + applyRouterMiddleware, + useRouterHistory, + Router, + Route, + IndexRedirect, + IndexRoute +} from 'react-router'; +import { useScroll } from 'react-router-scroll'; +import UI from '../features/ui'; +import Status from '../features/status'; +import GettingStarted from '../features/getting_started'; +import PublicTimeline from '../features/public_timeline'; +import CommunityTimeline from '../features/community_timeline'; +import AccountTimeline from '../features/account_timeline'; +import HomeTimeline from '../features/home_timeline'; +import Compose from '../features/compose'; +import Followers from '../features/followers'; +import Following from '../features/following'; +import Reblogs from '../features/reblogs'; +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 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 ar from 'react-intl/locale-data/ar'; +import en from 'react-intl/locale-data/en'; +import de from 'react-intl/locale-data/de'; +import eo from 'react-intl/locale-data/eo'; +import es from 'react-intl/locale-data/es'; +import fa from 'react-intl/locale-data/fa'; +import fi from 'react-intl/locale-data/fi'; +import fr from 'react-intl/locale-data/fr'; +import he from 'react-intl/locale-data/he'; +import hu from 'react-intl/locale-data/hu'; +import it from 'react-intl/locale-data/it'; +import ja from 'react-intl/locale-data/ja'; +import pt from 'react-intl/locale-data/pt'; +import nl from 'react-intl/locale-data/nl'; +import no from 'react-intl/locale-data/no'; +import ru from 'react-intl/locale-data/ru'; +import uk from 'react-intl/locale-data/uk'; +import zh from 'react-intl/locale-data/zh'; +import bg from 'react-intl/locale-data/bg'; +import id from 'react-intl/locale-data/id'; +import getMessagesForLocale from '../locales'; +import { hydrateStore } from '../actions/store'; +import createStream from '../stream'; + +const store = configureStore(); +const initialState = JSON.parse(document.getElementById("initial-state").textContent); +store.dispatch(hydrateStore(initialState)); + +const browserHistory = useRouterHistory(createBrowserHistory)({ + basename: '/web' +}); + +addLocaleData([ + ...en, + ...ar, + ...de, + ...eo, + ...es, + ...fa, + ...fi, + ...fr, + ...he, + ...hu, + ...it, + ...ja, + ...pt, + ...nl, + ...no, + ...ru, + ...uk, + ...zh, + ...bg, + ...id, +]); + +const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0]; + +const hiddenColumnContainerStyle = { + position: 'absolute', + left: '0', + top: '0', + visibility: 'hidden' +}; + +class Container extends React.PureComponent { + + constructor(props) { + super(props); + + this.state = { + renderedPersistents: [], + unrenderedPersistents: [], + }; + } + + componentWillMount () { + this.unlistenHistory = null; + + this.setState(() => { + return { + mountImpersistent: false, + renderedPersistents: [], + unrenderedPersistents: [ + {pathname: '/timelines/home', component: HomeTimeline}, + {pathname: '/timelines/public', component: PublicTimeline}, + {pathname: '/timelines/public/local', component: CommunityTimeline}, + + {pathname: '/notifications', component: Notifications}, + {pathname: '/favourites', component: FavouritedStatuses} + ], + }; + }, () => { + if (this.unlistenHistory) { + return; + } + + this.unlistenHistory = browserHistory.listen(location => { + const pathname = location.pathname.replace(/\/$/, '').toLowerCase(); + + this.setState(oldState => { + let persistentMatched = false; + + const newState = { + renderedPersistents: oldState.renderedPersistents.map(persistent => { + const givenMatched = persistent.pathname === pathname; + + if (givenMatched) { + persistentMatched = true; + } + + return { + hidden: !givenMatched, + pathname: persistent.pathname, + component: persistent.component + }; + }), + }; + + if (!persistentMatched) { + newState.unrenderedPersistents = []; + + oldState.unrenderedPersistents.forEach(persistent => { + if (persistent.pathname === pathname) { + persistentMatched = true; + + newState.renderedPersistents.push({ + hidden: false, + pathname: persistent.pathname, + component: persistent.component + }); + } else { + newState.unrenderedPersistents.push(persistent); + } + }); + } + + newState.mountImpersistent = !persistentMatched; + + return newState; + }); + }); + }); + } + + componentWillUnmount () { + if (this.unlistenHistory) { + this.unlistenHistory(); + } + + this.unlistenHistory = "done"; + } + + render () { + // Hide some components rather than unmounting them to allow to show again + // quickly and keep the view state such as the scrolled offset. + const persistentsView = this.state.renderedPersistents.map((persistent) => + <div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}> + <persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} /> + </div> + ); + + return ( + <UI> + {this.state.mountImpersistent && this.props.children} + {persistentsView} + </UI> + ); + } +} + +Container.propTypes = { + children: PropTypes.node, +}; + +class Mastodon extends React.Component { + + 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(streamingAPIBaseURL, accessToken, 'user', { + + connected () { + store.dispatch(connectTimeline('home')); + }, + + disconnected () { + store.dispatch(disconnectTimeline('home')); + }, + + received (data) { + switch(data.event) { + case 'update': + store.dispatch(updateTimeline('home', JSON.parse(data.payload))); + break; + case 'delete': + store.dispatch(deleteFromTimelines(data.payload)); + break; + case 'notification': + store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale)); + break; + } + }, + + reconnected () { + store.dispatch(connectTimeline('home')); + store.dispatch(refreshTimeline('home')); + store.dispatch(refreshNotifications()); + } + + }); + + // Desktop notifications + if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { + Notification.requestPermission(); + } + + store.dispatch(showOnboardingOnce()); + } + + componentWillUnmount () { + if (typeof this.subscription !== 'undefined') { + this.subscription.close(); + this.subscription = null; + } + } + + render () { + const { locale } = this.props; + + return ( + <IntlProvider locale={locale} messages={getMessagesForLocale(locale)}> + <Provider store={store}> + <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}> + <Route path='/' component={Container}> + <IndexRedirect to='/getting-started' /> + <Route path='getting-started' component={GettingStarted} /> + <Route path='timelines/tag/:id' component={HashtagTimeline} /> + + <Route path='statuses/new' component={Compose} /> + <Route path='statuses/:statusId' component={Status} /> + <Route path='statuses/:statusId/reblogs' component={Reblogs} /> + <Route path='statuses/:statusId/favourites' component={Favourites} /> + + <Route path='accounts/:accountId' component={AccountTimeline} /> + <Route path='accounts/:accountId/followers' component={Followers} /> + <Route path='accounts/:accountId/following' component={Following} /> + + <Route path='follow_requests' component={FollowRequests} /> + <Route path='blocks' component={Blocks} /> + <Route path='mutes' component={Mutes} /> + <Route path='report' component={Report} /> + + <Route path='*' component={GenericNotFound} /> + </Route> + </Router> + </Provider> + </IntlProvider> + ); + } + +} + +Mastodon.propTypes = { + locale: PropTypes.string.isRequired +}; + +export default Mastodon; diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js @@ -0,0 +1,118 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Status from '../components/status'; +import { makeGetStatus } from '../selectors'; +import { + replyCompose, + mentionCompose +} from '../actions/compose'; +import { + reblog, + favourite, + unreblog, + unfavourite +} from '../actions/interactions'; +import { + blockAccount, + muteAccount +} from '../actions/accounts'; +import { deleteStatus } from '../actions/statuses'; +import { initReport } from '../actions/reports'; +import { openModal } from '../actions/modal'; +import { createSelector } from 'reselect' +import { isMobile } from '../is_mobile' +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +const messages = defineMessages({ + deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, + deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, + blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, + muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => ({ + status: getStatus(state, props.id), + me: state.getIn(['meta', 'me']), + boostModal: state.getIn(['meta', 'boost_modal']), + autoPlayGif: state.getIn(['meta', 'auto_play_gif']) + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + + onReply (status, router) { + dispatch(replyCompose(status, router)); + }, + + onModalReblog (status) { + dispatch(reblog(status)); + }, + + onReblog (status, e) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + if (e.shiftKey || !this.boostModal) { + this.onModalReblog(status); + } else { + dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); + } + } + }, + + onFavourite (status) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }, + + onDelete (status) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'))) + })); + }, + + onMention (account, router) { + dispatch(mentionCompose(account, router)); + }, + + onOpenMedia (media, index) { + dispatch(openModal('MEDIA', { media, index })); + }, + + onOpenVideo (media, time) { + dispatch(openModal('VIDEO', { media, time })); + }, + + onBlock (account) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.blockConfirm), + onConfirm: () => dispatch(blockAccount(account.get('id'))) + })); + }, + + onReport (status) { + dispatch(initReport(status.get('account'), status)); + }, + + onMute (account) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.muteConfirm), + onConfirm: () => dispatch(muteAccount(account.get('id'))) + })); + }, + +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/assets/javascripts/components/emoji.jsx b/app/javascript/mastodon/emoji.js diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js @@ -0,0 +1,93 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import DropdownMenu from '../../../components/dropdown_menu'; +import { Link } from 'react-router'; +import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; + +const messages = defineMessages({ + mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } +}); + +class ActionBar extends React.PureComponent { + + render () { + const { account, me, intl } = this.props; + + let menu = []; + let extraInfo = ''; + + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + menu.push(null); + + if (account.get('id') === me) { + menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + } else { + if (account.getIn(['relationship', 'muting'])) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + } + + if (account.getIn(['relationship', 'blocking'])) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + } + + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); + } + + if (account.get('acct') !== account.get('username')) { + extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>; + } + + return ( + <div className='account__action-bar'> + <div className='account__action-bar-dropdown'> + <DropdownMenu items={menu} icon='bars' size={24} direction="right" /> + </div> + + <div className='account__action-bar-links'> + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> + <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span> + <strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong> + </Link> + + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> + <span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span> + <strong><FormattedNumber value={account.get('following_count')} /> {extraInfo}</strong> + </Link> + + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> + <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> + <strong><FormattedNumber value={account.get('followers_count')} /> {extraInfo}</strong> + </Link> + </div> + </div> + ); + } + +} + +ActionBar.propTypes = { + account: ImmutablePropTypes.map.isRequired, + me: PropTypes.number.isRequired, + onFollow: PropTypes.func, + onBlock: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, + onReport: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(ActionBar); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js @@ -0,0 +1,150 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import emojify from '../../../emoji'; +import escapeTextContentForBrowser from 'escape-html'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import IconButton from '../../../components/icon_button'; +import { Motion, spring } from 'react-motion'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } +}); + +const makeMapStateToProps = () => { + const mapStateToProps = (state, props) => ({ + autoPlayGif: state.getIn(['meta', 'auto_play_gif']) + }); + + return mapStateToProps; +}; + +class Avatar extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + + this.state = { + isHovered: false + }; + + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + } + + handleMouseOver () { + if (this.state.isHovered) return; + this.setState({ isHovered: true }); + } + + handleMouseOut () { + if (!this.state.isHovered) return; + this.setState({ isHovered: false }); + } + + render () { + const { account, autoPlayGif } = this.props; + const { isHovered } = this.state; + + return ( + <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> + {({ radius }) => + <a + href={account.get('url')} + className='account__header__avatar' + target='_blank' + rel='noopener' + style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }} + onMouseOver={this.handleMouseOver} + onMouseOut={this.handleMouseOut} + onFocus={this.handleMouseOver} + onBlur={this.handleMouseOut} + /> + } + </Motion> + ); + } + +} + +Avatar.propTypes = { + account: ImmutablePropTypes.map.isRequired, + autoPlayGif: PropTypes.bool.isRequired +}; + +class Header extends ImmutablePureComponent { + + render () { + const { account, me, intl } = this.props; + + if (!account) { + return null; + } + + let displayName = account.get('display_name'); + let info = ''; + let actionBtn = ''; + let lockedIcon = ''; + + if (displayName.length === 0) { + displayName = account.get('username'); + } + + if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { + info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span> + } + + if (me !== account.get('id')) { + if (account.getIn(['relationship', 'requested'])) { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> + </div> + ); + } else if (!account.getIn(['relationship', 'blocking'])) { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> + </div> + ); + } + } + + if (account.get('locked')) { + lockedIcon = <i className='fa fa-lock' />; + } + + const content = { __html: emojify(account.get('note')) }; + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + + return ( + <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> + <div style={{ padding: '20px 10px' }}> + <Avatar account={account} autoPlayGif={this.props.autoPlayGif} /> + + <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} /> + <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span> + <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} /> + + {info} + {actionBtn} + </div> + </div> + ); + } + +} + +Header.propTypes = { + account: ImmutablePropTypes.map, + me: PropTypes.number.isRequired, + onFollow: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + autoPlayGif: PropTypes.bool.isRequired +}; + +export default connect(makeMapStateToProps)(injectIntl(Header)); diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -0,0 +1,83 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import InnerHeader from '../../account/components/header'; +import ActionBar from '../../account/components/action_bar'; +import MissingIndicator from '../../../components/missing_indicator'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class Header extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleFollow = this.handleFollow.bind(this); + this.handleBlock = this.handleBlock.bind(this); + this.handleMention = this.handleMention.bind(this); + this.handleReport = this.handleReport.bind(this); + this.handleMute = this.handleMute.bind(this); + } + + handleFollow () { + this.props.onFollow(this.props.account); + } + + handleBlock () { + this.props.onBlock(this.props.account); + } + + handleMention () { + this.props.onMention(this.props.account, this.context.router); + } + + handleReport () { + this.props.onReport(this.props.account); + this.context.router.push('/report'); + } + + handleMute() { + this.props.onMute(this.props.account); + } + + render () { + const { account, me } = this.props; + + if (account === null) { + return <MissingIndicator />; + } + + return ( + <div className='account-timeline__header'> + <InnerHeader + account={account} + me={me} + onFollow={this.handleFollow} + /> + + <ActionBar + account={account} + me={me} + onBlock={this.handleBlock} + onMention={this.handleMention} + onReport={this.handleReport} + onMute={this.handleMute} + /> + </div> + ); + } +} + +Header.propTypes = { + account: ImmutablePropTypes.map, + me: PropTypes.number.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, + onReport: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired +}; + +Header.contextTypes = { + router: PropTypes.object +}; + +export default Header; diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { makeGetAccount } from '../../../selectors'; +import Header from '../components/header'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, + muteAccount, + unmuteAccount +} from '../../../actions/accounts'; +import { mentionCompose } from '../../../actions/compose'; +import { initReport } from '../../../actions/reports'; +import { openModal } from '../../../actions/modal'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +const messages = defineMessages({ + blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, + muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' } +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId }) => ({ + account: getAccount(state, Number(accountId)), + me: state.getIn(['meta', 'me']) + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onFollow (account) { + if (account.getIn(['relationship', 'following'])) { + dispatch(unfollowAccount(account.get('id'))); + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.blockConfirm), + onConfirm: () => dispatch(blockAccount(account.get('id'))) + })); + } + }, + + onMention (account, router) { + dispatch(mentionCompose(account, router)); + }, + + onReport (account) { + dispatch(initReport(account)); + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.muteConfirm), + onConfirm: () => dispatch(muteAccount(account.get('id'))) + })); + } + } +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { + fetchAccount, + fetchAccountTimeline, + expandAccountTimeline +} from '../../actions/accounts'; +import StatusList from '../../components/status_list'; +import LoadingIndicator from '../../components/loading_indicator'; +import Column from '../ui/components/column'; +import HeaderContainer from './containers/header_container'; +import ColumnBackButton from '../../components/column_back_button'; +import Immutable from 'immutable'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const mapStateToProps = (state, props) => ({ + statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()), + isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']), + hasMore: !!state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'next']), + me: state.getIn(['meta', 'me']) +}); + +class AccountTimeline extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScrollToBottom = this.handleScrollToBottom.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId))); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { + this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); + this.props.dispatch(fetchAccountTimeline(Number(nextProps.params.accountId))); + } + } + + handleScrollToBottom () { + if (!this.props.isLoading && this.props.hasMore) { + this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId))); + } + } + + render () { + const { statusIds, isLoading, hasMore, me } = this.props; + + if (!statusIds && isLoading) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column> + <ColumnBackButton /> + + <StatusList + prepend={<HeaderContainer accountId={this.props.params.accountId} />} + scrollKey='account_timeline' + statusIds={statusIds} + isLoading={isLoading} + hasMore={hasMore} + me={me} + onScrollToBottom={this.handleScrollToBottom} + /> + </Column> + ); + } + +} + +AccountTimeline.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + statusIds: ImmutablePropTypes.list, + isLoading: PropTypes.bool, + hasMore: PropTypes.bool, + me: PropTypes.number.isRequired +}; + +export default connect(mapStateToProps)(AccountTimeline); diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +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 { fetchBlocks, expandBlocks } from '../../actions/blocks'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + heading: { id: 'column.blocks', defaultMessage: 'Blocked users' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'blocks', 'items']) +}); + +class Blocks extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchBlocks()); + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandBlocks()); + } + } + + render () { + const { intl, accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='ban' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <ScrollContainer scrollKey='blocks'> + <div className='scrollable' onScroll={this.handleScroll}> + {accountIds.map(id => + <AccountContainer key={id} id={id} /> + )} + </div> + </ScrollContainer> + </Column> + ); + } +} + +Blocks.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + intl: PropTypes.object.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../ui/components/column'; +import { + refreshTimeline, + updateTimeline, + deleteFromTimelines, + connectTimeline, + disconnectTimeline +} from '../../actions/timelines'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import createStream from '../../stream'; + +const messages = defineMessages({ + title: { id: 'column.community', defaultMessage: 'Local timeline' } +}); + +const mapStateToProps = state => ({ + hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, + streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), + accessToken: state.getIn(['meta', 'access_token']) +}); + +let subscription; + +class CommunityTimeline extends React.PureComponent { + + componentDidMount () { + const { dispatch, streamingAPIBaseURL, accessToken } = this.props; + + dispatch(refreshTimeline('community')); + + if (typeof subscription !== 'undefined') { + return; + } + + subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', { + + connected () { + dispatch(connectTimeline('community')); + }, + + reconnected () { + dispatch(connectTimeline('community')); + }, + + disconnected () { + dispatch(disconnectTimeline('community')); + }, + + received (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline('community', JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; + } + } + + }); + } + + componentWillUnmount () { + // if (typeof subscription !== 'undefined') { + // subscription.close(); + // subscription = null; + // } + } + + render () { + const { intl, hasUnread } = this.props; + + return ( + <Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}> + <ColumnBackButtonSlim /> + <StatusListContainer {...this.props} scrollKey='community_timeline' type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} /> + </Column> + ); + } + +} + +CommunityTimeline.propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + streamingAPIBaseURL: PropTypes.string.isRequired, + accessToken: PropTypes.string.isRequired, + hasUnread: PropTypes.bool +}; + +export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.js b/app/javascript/mastodon/features/compose/components/autosuggest_account.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class AutosuggestAccount extends ImmutablePureComponent { + + render () { + const { account } = this.props; + + return ( + <div className='autosuggest-account'> + <div className='autosuggest-account-icon'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div> + <DisplayName account={account} /> + </div> + ); + } + +} + +AutosuggestAccount.propTypes = { + account: ImmutablePropTypes.map.isRequired +}; + +export default AutosuggestAccount; diff --git a/app/javascript/mastodon/features/compose/components/character_counter.js b/app/javascript/mastodon/features/compose/components/character_counter.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { length } from 'stringz'; + +class CharacterCounter extends React.PureComponent { + + checkRemainingText (diff) { + if (diff < 0) { + return <span className='character-counter character-counter--over'>{diff}</span>; + } + return <span className='character-counter'>{diff}</span>; + } + + render () { + const diff = this.props.max - length(this.props.text); + + return this.checkRemainingText(diff); + } + +} + +CharacterCounter.propTypes = { + text: PropTypes.string.isRequired, + max: PropTypes.number.isRequired +} + +export default CharacterCounter; diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -0,0 +1,211 @@ +import React from 'react'; +import CharacterCounter from './character_counter'; +import Button from '../../../components/button'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import { debounce } from 'react-decoration'; +import UploadButtonContainer from '../containers/upload_button_container'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Toggle from 'react-toggle'; +import Collapsable from '../../../components/collapsable'; +import SpoilerButtonContainer from '../containers/spoiler_button_container'; +import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; +import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import EmojiPickerDropdown from './emoji_picker_dropdown'; +import UploadFormContainer from '../containers/upload_form_container'; +import TextIconButton from './text_icon_button'; +import WarningContainer from '../containers/warning_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, + spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' }, + publish: { id: 'compose_form.publish', defaultMessage: 'Toot' } +}); + +class ComposeForm extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this); + this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); + this.onSuggestionSelected = this.onSuggestionSelected.bind(this); + this.handleChangeSpoilerText = this.handleChangeSpoilerText.bind(this); + this.setAutosuggestTextarea = this.setAutosuggestTextarea.bind(this); + this.handleEmojiPick = this.handleEmojiPick.bind(this); + } + + handleChange (e) { + this.props.onChange(e.target.value); + } + + handleKeyDown (e) { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.handleSubmit(); + } + } + + handleSubmit () { + this.autosuggestTextarea.reset(); + this.props.onSubmit(); + } + + onSuggestionsClearRequested () { + this.props.onClearSuggestions(); + } + + @debounce(500) + onSuggestionsFetchRequested (token) { + this.props.onFetchSuggestions(token); + } + + onSuggestionSelected (tokenStart, token, value) { + this._restoreCaret = null; + this.props.onSuggestionSelected(tokenStart, token, value); + } + + handleChangeSpoilerText (e) { + this.props.onChangeSpoilerText(e.target.value); + } + + componentWillReceiveProps (nextProps) { + // If this is the update where we've finished uploading, + // save the last caret position so we can restore it below! + if (!nextProps.is_uploading && this.props.is_uploading) { + this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; + } + } + + componentDidUpdate (prevProps) { + // 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; + // this provides a convenient shortcut to drop everyone else from the conversation. + // - If we've just finished uploading an image, and have a saved caret position, + // restores the cursor to that position after the text changes! + if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { + let selectionEnd, selectionStart; + + if (this.props.preselectDate !== prevProps.preselectDate) { + selectionEnd = this.props.text.length; + selectionStart = this.props.text.search(/\s/) + 1; + } else if (typeof this._restoreCaret === 'number') { + selectionStart = this._restoreCaret; + selectionEnd = this._restoreCaret; + } else { + selectionEnd = this.props.text.length; + selectionStart = selectionEnd; + } + + this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); + this.autosuggestTextarea.textarea.focus(); + } + } + + setAutosuggestTextarea (c) { + this.autosuggestTextarea = c; + } + + handleEmojiPick (data) { + const position = this.autosuggestTextarea.textarea.selectionStart; + this._restoreCaret = position + data.shortname.length + 1; + this.props.onPickEmoji(position, data); + } + + render () { + const { intl, onPaste } = this.props; + const disabled = this.props.is_submitting; + const text = [this.props.spoiler_text, this.props.text].join(''); + + let publishText = ''; + let reply_to_other = false; + + if (this.props.privacy === 'private' || this.props.privacy === 'direct') { + publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; + } else { + publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== 'unlisted' ? '!' : ''); + } + + return ( + <div className='compose-form'> + <Collapsable isVisible={this.props.spoiler} fullHeight={50}> + <div className="spoiler-input"> + <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" id='cw-spoiler-input'/> + </div> + </Collapsable> + + <WarningContainer /> + + <ReplyIndicatorContainer /> + + <div className='compose-form__autosuggest-wrapper'> + <AutosuggestTextarea + ref={this.setAutosuggestTextarea} + placeholder={intl.formatMessage(messages.placeholder)} + disabled={disabled} + value={this.props.text} + onChange={this.handleChange} + suggestions={this.props.suggestions} + onKeyDown={this.handleKeyDown} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + onPaste={onPaste} + /> + + <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> + </div> + + <div className='compose-form__modifiers'> + <UploadFormContainer /> + </div> + + <div className='compose-form__buttons-wrapper'> + <div className='compose-form__buttons'> + <UploadButtonContainer /> + <PrivacyDropdownContainer /> + <SensitiveButtonContainer /> + <SpoilerButtonContainer /> + </div> + + <div className='compose-form__publish'> + <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> + <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500 || (text.length !==0 && text.trim().length === 0)} block /></div> + </div> + </div> + </div> + ); + } + +} + +ComposeForm.propTypes = { + intl: PropTypes.object.isRequired, + text: PropTypes.string.isRequired, + suggestion_token: PropTypes.string, + suggestions: ImmutablePropTypes.list, + spoiler: PropTypes.bool, + privacy: PropTypes.string, + spoiler_text: PropTypes.string, + focusDate: PropTypes.instanceOf(Date), + preselectDate: PropTypes.instanceOf(Date), + is_submitting: PropTypes.bool, + is_uploading: PropTypes.bool, + me: PropTypes.number, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onClearSuggestions: PropTypes.func.isRequired, + onFetchSuggestions: PropTypes.func.isRequired, + onSuggestionSelected: PropTypes.func.isRequired, + onChangeSpoilerText: PropTypes.func.isRequired, + onPaste: PropTypes.func.isRequired, + onPickEmoji: PropTypes.func.isRequired +}; + +export default injectIntl(ComposeForm); diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -0,0 +1,115 @@ +import React from 'react'; +import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; +import EmojiPicker from 'emojione-picker'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, + emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, + people: { id: 'emoji_button.people', defaultMessage: 'People' }, + nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, + food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, + activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, + travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, + objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, + symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, + flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' } +}); + +const settings = { + imageType: 'png', + sprites: false, + imagePathPNG: '/emoji/' +}; + +const dropdownStyle = { + position: 'absolute', + right: '5px', + top: '5px' +}; + +const dropdownTriggerStyle = { + display: 'block', + fontSize: '24px', + lineHeight: '24px', + marginLeft: '2px', + width: '24px' +} + +class EmojiPickerDropdown extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.setRef = this.setRef.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + setRef (c) { + this.dropdown = c; + } + + handleChange (data) { + this.dropdown.hide(); + this.props.onPickEmoji(data); + } + + render () { + const { intl } = this.props; + + const categories = { + people: { + title: intl.formatMessage(messages.people), + emoji: 'smile', + }, + nature: { + title: intl.formatMessage(messages.nature), + emoji: 'hamster', + }, + food: { + title: intl.formatMessage(messages.food), + emoji: 'pizza', + }, + activity: { + title: intl.formatMessage(messages.activity), + emoji: 'soccer', + }, + travel: { + title: intl.formatMessage(messages.travel), + emoji: 'earth_americas', + }, + objects: { + title: intl.formatMessage(messages.objects), + emoji: 'bulb', + }, + symbols: { + title: intl.formatMessage(messages.symbols), + emoji: 'clock9', + }, + flags: { + title: intl.formatMessage(messages.flags), + emoji: 'flag_gb', + } + } + + return ( + <Dropdown ref={this.setRef} style={dropdownStyle}> + <DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={dropdownTriggerStyle}> + <img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" /> + </DropdownTrigger> + + <DropdownContent className='dropdown__left'> + <EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search={true} /> + </DropdownContent> + </Dropdown> + ); + } + +} + +EmojiPickerDropdown.propTypes = { + intl: PropTypes.object.isRequired, + onPickEmoji: PropTypes.func.isRequired +}; + +export default injectIntl(EmojiPickerDropdown); diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import IconButton from '../../../components/icon_button'; +import DisplayName from '../../../components/display_name'; +import Permalink from '../../../components/permalink'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class NavigationBar extends ImmutablePureComponent { + + render () { + return ( + <div className='navigation-bar'> + <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <Avatar src={this.props.account.get('avatar')} animate size={40} /> + </Permalink> + + <div className='navigation-bar__profile'> + <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> + </Permalink> + + <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> + </div> + </div> + ); + } + +} + +NavigationBar.propTypes = { + account: ImmutablePropTypes.map.isRequired +}; + +export default NavigationBar; diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' } +}); + +const iconStyle = { + height: null, + lineHeight: '27px' +} + +class PrivacyDropdown extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + open: false + }; + this.handleToggle = this.handleToggle.bind(this); + this.handleClick = this.handleClick.bind(this); + this.onGlobalClick = this.onGlobalClick.bind(this); + this.setRef = this.setRef.bind(this); + } + + handleToggle () { + this.setState({ open: !this.state.open }); + } + + handleClick (value, e) { + e.preventDefault(); + this.setState({ open: false }); + this.props.onChange(value); + } + + onGlobalClick (e) { + if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { + this.setState({ open: false }); + } + } + + componentDidMount () { + window.addEventListener('click', this.onGlobalClick); + window.addEventListener('touchstart', this.onGlobalClick); + } + + componentWillUnmount () { + window.removeEventListener('click', this.onGlobalClick); + window.removeEventListener('touchstart', this.onGlobalClick); + } + + setRef (c) { + this.node = c; + } + + render () { + const { value, onChange, intl } = this.props; + const { open } = this.state; + + const options = [ + { icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) }, + { icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) }, + { icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) }, + { icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) } + ]; + + const valueOption = options.find(item => item.value === value); + + return ( + <div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}> + <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div> + <div className='privacy-dropdown__dropdown'> + {options.map(item => + <div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> + <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div> + <div className='privacy-dropdown__option__content'> + <strong>{item.shortText}</strong> + {item.longText} + </div> + </div> + )} + </div> + </div> + ); + } + +} + +PrivacyDropdown.propTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(PrivacyDropdown); diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js @@ -0,0 +1,71 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from '../../../components/avatar'; +import IconButton from '../../../components/icon_button'; +import DisplayName from '../../../components/display_name'; +import emojify from '../../../emoji'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' } +}); + +class ReplyIndicator extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + this.handleAccountClick = this.handleAccountClick.bind(this); + } + + handleClick () { + this.props.onCancel(); + } + + handleAccountClick (e) { + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + } + } + + render () { + const { status, intl } = this.props; + + if (!status) { + return null; + } + + const content = { __html: emojify(status.get('content')) }; + + return ( + <div className='reply-indicator'> + <div className='reply-indicator__header'> + <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> + + <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'> + <div className='reply-indicator__display-avatar'><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div> + <DisplayName account={status.get('account')} /> + </a> + </div> + + <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> + </div> + ); + } + +} + +ReplyIndicator.contextTypes = { + router: PropTypes.object +}; + +ReplyIndicator.propTypes = { + status: ImmutablePropTypes.map, + onCancel: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(ReplyIndicator); diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +const messages = defineMessages({ + placeholder: { id: 'search.placeholder', defaultMessage: 'Search' } +}); + +class Search extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleFocus = this.handleFocus.bind(this); + this.handleClear = this.handleClear.bind(this); + } + + handleChange (e) { + this.props.onChange(e.target.value); + } + + handleClear (e) { + e.preventDefault(); + + if (this.props.value.length > 0 || this.props.submitted) { + this.props.onClear(); + } + } + + handleKeyDown (e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.props.onSubmit(); + } + } + + noop () { + + } + + handleFocus () { + this.props.onShow(); + } + + render () { + const { intl, value, submitted } = this.props; + const hasValue = value.length > 0 || submitted; + + return ( + <div className='search'> + <input + className='search__input' + type='text' + placeholder={intl.formatMessage(messages.placeholder)} + value={value} + onChange={this.handleChange} + onKeyUp={this.handleKeyDown} + onFocus={this.handleFocus} + /> + + <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> + <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> + <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> + </div> + </div> + ); + } + +} + +Search.propTypes = { + value: PropTypes.string.isRequired, + submitted: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + onShow: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(Search); diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js @@ -0,0 +1,67 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import AccountContainer from '../../../containers/account_container'; +import StatusContainer from '../../../containers/status_container'; +import { Link } from 'react-router'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class SearchResults extends ImmutablePureComponent { + + render () { + const { results } = this.props; + + let accounts, statuses, hashtags; + let count = 0; + + if (results.get('accounts') && results.get('accounts').size > 0) { + count += results.get('accounts').size; + accounts = ( + <div className='search-results__section'> + {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} + </div> + ); + } + + if (results.get('statuses') && results.get('statuses').size > 0) { + count += results.get('statuses').size; + statuses = ( + <div className='search-results__section'> + {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} + </div> + ); + } + + if (results.get('hashtags') && results.get('hashtags').size > 0) { + count += results.get('hashtags').size; + hashtags = ( + <div className='search-results__section'> + {results.get('hashtags').map(hashtag => + <Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> + #{hashtag} + </Link> + )} + </div> + ); + } + + return ( + <div className='search-results'> + <div className='search-results__header'> + <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> + </div> + + {accounts} + {statuses} + {hashtags} + </div> + ); + } + +} + +SearchResults.propTypes = { + results: ImmutablePropTypes.map.isRequired +}; + +export default SearchResults; diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class TextIconButton extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick (e) { + e.preventDefault(); + this.props.onClick(); + } + + render () { + const { label, title, active, ariaControls } = this.props; + + return ( + <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> + {label} + </button> + ); + } + +} + +TextIconButton.propTypes = { + label: PropTypes.string.isRequired, + title: PropTypes.string, + active: PropTypes.bool, + onClick: PropTypes.func.isRequired, + ariaControls: PropTypes.string +}; + +export default TextIconButton; diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -0,0 +1,61 @@ +import React from 'react'; +import IconButton from '../../../components/icon_button'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + upload: { id: 'upload_button.label', defaultMessage: 'Add media' } +}); + + +const iconStyle = { + height: null, + lineHeight: '27px' +} + +class UploadButton extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleChange = this.handleChange.bind(this); + this.handleClick = this.handleClick.bind(this); + this.setRef = this.setRef.bind(this); + } + + handleChange (e) { + if (e.target.files.length > 0) { + this.props.onSelectFile(e.target.files); + } + } + + handleClick () { + this.fileElement.click(); + } + + setRef (c) { + this.fileElement = c; + } + + render () { + + const { intl, resetFileKey, disabled } = this.props; + + return ( + <div className='compose-form__upload-button'> + <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle}/> + <input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} /> + </div> + ); + } + +} + +UploadButton.propTypes = { + disabled: PropTypes.bool, + onSelectFile: PropTypes.func.isRequired, + style: PropTypes.object, + resetFileKey: PropTypes.number, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(UploadButton); diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -0,0 +1,46 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import UploadProgressContainer from '../containers/upload_progress_container'; +import { Motion, spring } from 'react-motion'; + +const messages = defineMessages({ + undo: { id: 'upload_form.undo', defaultMessage: 'Undo' } +}); + +class UploadForm extends React.PureComponent { + + render () { + const { intl, media } = this.props; + + const uploads = media.map(attachment => + <div className='compose-form__upload' key={attachment.get('id')}> + <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> + {({ scale }) => + <div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${attachment.get('preview_url')})` }}> + <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> + </div> + } + </Motion> + </div> + ); + + return ( + <div className='compose-form__upload-wrapper'> + <UploadProgressContainer /> + <div className='compose-form__uploads-wrapper'>{uploads}</div> + </div> + ); + } + +} + +UploadForm.propTypes = { + media: ImmutablePropTypes.list.isRequired, + onRemoveFile: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(UploadForm); diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Motion, spring } from 'react-motion'; +import { FormattedMessage } from 'react-intl'; + +class UploadProgress extends React.PureComponent { + + render () { + const { active, progress } = this.props; + + if (!active) { + return null; + } + + return ( + <div className='upload-progress'> + <div className='upload-progress__icon'> + <i className='fa fa-upload' /> + </div> + + <div className='upload-progress__message'> + <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + + <div className='upload-progress__backdrop'> + <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> + {({ width }) => + <div className='upload-progress__tracker' style={{ width: `${width}%` }} /> + } + </Motion> + </div> + </div> + </div> + ); + } + +} + +UploadProgress.propTypes = { + active: PropTypes.bool, + progress: PropTypes.number +}; + +export default UploadProgress; diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Warning extends React.PureComponent { + + constructor (props) { + super(props); + } + + render () { + const { message } = this.props; + + return ( + <div className='compose-form__warning'> + {message} + </div> + ); + } + +} + +Warning.propTypes = { + message: PropTypes.node.isRequired +}; + +export default Warning; diff --git a/app/assets/javascripts/components/features/compose/containers/autosuggest_account_container.jsx b/app/javascript/mastodon/features/compose/containers/autosuggest_account_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx b/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/javascript/mastodon/features/compose/containers/compose_form_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx b/app/javascript/mastodon/features/compose/containers/navigation_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/privacy_dropdown_container.jsx b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx b/app/javascript/mastodon/features/compose/containers/reply_indicator_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/search_container.jsx b/app/javascript/mastodon/features/compose/containers/search_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx b/app/javascript/mastodon/features/compose/containers/search_results_container.js diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import TextIconButton from '../components/text_icon_button'; +import { changeComposeSensitivity } from '../../../actions/compose'; +import { Motion, spring } from 'react-motion'; +import { injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' } +}); + +const mapStateToProps = state => ({ + visible: state.getIn(['compose', 'media_attachments']).size > 0, + active: state.getIn(['compose', 'sensitive']) +}); + +const mapDispatchToProps = dispatch => ({ + + onClick () { + dispatch(changeComposeSensitivity()); + } + +}); + +class SensitiveButton extends React.PureComponent { + + render () { + const { visible, active, onClick, intl } = this.props; + + return ( + <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> + {({ scale }) => + <div style={{ display: visible ? 'block' : 'none', transform: `translateZ(0) scale(${scale})` }}> + <TextIconButton onClick={onClick} label='NSFW' title={intl.formatMessage(messages.title)} active={active} /> + </div> + } + </Motion> + ); + } + +} + +SensitiveButton.propTypes = { + visible: PropTypes.bool, + active: PropTypes.bool, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx b/app/javascript/mastodon/features/compose/containers/spoiler_button_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/upload_button_container.jsx b/app/javascript/mastodon/features/compose/containers/upload_button_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/upload_form_container.jsx b/app/javascript/mastodon/features/compose/containers/upload_form_container.js diff --git a/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx b/app/javascript/mastodon/features/compose/containers/upload_progress_container.js diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Warning from '../components/warning'; +import { createSelector } from 'reselect'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig)); + +const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => { + return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []; +}); + +const mapStateToProps = state => { + const mentionedUsernames = getMentionedUsernames(state); + const mentionedUsernamesWithDomains = getMentionedDomains(state); + + return { + needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null, + mentionedDomains: mentionedUsernamesWithDomains, + needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']) + }; +}; + +const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => { + if (needsLockWarning) { + return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; + } else if (needsLeakWarning) { + return ( + <Warning + message={<FormattedMessage + id='compose_form.privacy_disclaimer' + defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.' + values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }} + />} + /> + ); + } + + return null; +}; + +WarningWrapper.propTypes = { + needsLeakWarning: PropTypes.bool, + needsLockWarning: PropTypes.bool, + mentionedDomains: PropTypes.array.isRequired, +}; + +export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js @@ -0,0 +1,86 @@ +import React from 'react'; +import ComposeFormContainer from './containers/compose_form_container'; +import UploadFormContainer from './containers/upload_form_container'; +import NavigationContainer from './containers/navigation_container'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { mountCompose, unmountCompose } from '../../actions/compose'; +import { Link } from 'react-router'; +import { injectIntl, defineMessages } from 'react-intl'; +import SearchContainer from './containers/search_container'; +import { Motion, spring } from 'react-motion'; +import SearchResultsContainer from './containers/search_results_container'; + +const messages = defineMessages({ + start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, + community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } +}); + +const mapStateToProps = state => ({ + showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) +}); + +class Compose extends React.PureComponent { + + componentDidMount () { + this.props.dispatch(mountCompose()); + } + + componentWillUnmount () { + this.props.dispatch(unmountCompose()); + } + + render () { + const { withHeader, showSearch, intl } = this.props; + + let header = ''; + + if (withHeader) { + header = ( + <div className='drawer__header'> + <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role="img" aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link> + <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role="img" aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> + <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role="img" aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> + <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role="img" aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a> + <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role="img" aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a> + </div> + ); + } + + return ( + <div className='drawer'> + {header} + + <SearchContainer /> + + <div className='drawer__pager'> + <div className='drawer__inner'> + <NavigationContainer /> + <ComposeFormContainer /> + </div> + + <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> + {({ x }) => + <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> + <SearchResultsContainer /> + </div> + } + </Motion> + </div> + </div> + ); + } + +} + +Compose.propTypes = { + dispatch: PropTypes.func.isRequired, + withHeader: PropTypes.bool, + showSearch: PropTypes.bool, + intl: PropTypes.object.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(Compose)); diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +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 ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +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']) +}); + +class Favourites extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScrollToBottom = this.handleScrollToBottom.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchFavouritedStatuses()); + } + + handleScrollToBottom () { + this.props.dispatch(expandFavouritedStatuses()); + } + + render () { + const { statusIds, loaded, intl, me } = this.props; + + if (!loaded) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='star' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <StatusList {...this.props} scrollKey='favourited_statuses' onScrollToBottom={this.handleScrollToBottom} /> + </Column> + ); + } + +} + +Favourites.propTypes = { + dispatch: PropTypes.func.isRequired, + statusIds: ImmutablePropTypes.list.isRequired, + loaded: PropTypes.bool, + intl: PropTypes.object.isRequired, + me: PropTypes.number.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(Favourites)); diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { fetchFavourites } from '../../actions/interactions'; +import { ScrollContainer } from 'react-router-scroll'; +import AccountContainer from '../../containers/account_container'; +import Column from '../ui/components/column'; +import ColumnBackButton from '../../components/column_back_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const mapStateToProps = (state, props) => ({ + accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]) +}); + +class Favourites extends ImmutablePureComponent { + + componentWillMount () { + this.props.dispatch(fetchFavourites(Number(this.props.params.statusId))); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { + this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId))); + } + } + + render () { + const { accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column> + <ColumnBackButton /> + + <ScrollContainer scrollKey='favourites'> + <div className='scrollable'> + {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Favourites.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list +}; + +export default connect(mapStateToProps)(Favourites); diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Permalink from '../../../components/permalink'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import emojify from '../../../emoji'; +import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, + reject: { id: 'follow_request.reject', defaultMessage: 'Reject' } +}); + +class AccountAuthorize extends ImmutablePureComponent { + + render () { + const { intl, account, onAuthorize, onReject } = this.props; + const content = { __html: emojify(account.get('note')) }; + + return ( + <div className='account-authorize__wrapper'> + <div className='account-authorize'> + <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'> + <div className='account-authorize__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div> + <DisplayName account={account} /> + </Permalink> + + <div className='account__header__content' dangerouslySetInnerHTML={content} /> + </div> + + <div className='account--panel'> + <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div> + <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div> + </div> + </div> + ); + } + +} + +AccountAuthorize.propTypes = { + account: ImmutablePropTypes.map.isRequired, + onAuthorize: PropTypes.func.isRequired, + onReject: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(AccountAuthorize); diff --git a/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx b/app/javascript/mastodon/features/follow_requests/containers/account_authorize_container.js diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +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 AccountAuthorizeContainer from './containers/account_authorize_container'; +import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'follow_requests', 'items']) +}); + +class FollowRequests extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchFollowRequests()); + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandFollowRequests()); + } + } + + render () { + const { intl, accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='users' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <ScrollContainer scrollKey='follow_requests'> + <div className='scrollable' onScroll={this.handleScroll}> + {accountIds.map(id => + <AccountAuthorizeContainer key={id} id={id} /> + )} + </div> + </ScrollContainer> + </Column> + ); + } +} + +FollowRequests.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + intl: PropTypes.object.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(FollowRequests)); diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { + fetchAccount, + fetchFollowers, + expandFollowers +} from '../../actions/accounts'; +import { ScrollContainer } from 'react-router-scroll'; +import AccountContainer from '../../containers/account_container'; +import Column from '../ui/components/column'; +import HeaderContainer from '../account_timeline/containers/header_container'; +import LoadMore from '../../components/load_more'; +import ColumnBackButton from '../../components/column_back_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const mapStateToProps = (state, props) => ({ + accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']) +}); + +class Followers extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + this.handleLoadMore = this.handleLoadMore.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); + this.props.dispatch(fetchFollowers(Number(this.props.params.accountId))); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { + this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); + this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId))); + } + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); + } + } + + handleLoadMore (e) { + e.preventDefault(); + this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); + } + + render () { + const { accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column> + <ColumnBackButton /> + + <ScrollContainer scrollKey='followers'> + <div className='scrollable' onScroll={this.handleScroll}> + <div className='followers'> + <HeaderContainer accountId={this.props.params.accountId} /> + {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} + <LoadMore onClick={this.handleLoadMore} /> + </div> + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Followers.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list +}; + +export default connect(mapStateToProps)(Followers); diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { + fetchAccount, + fetchFollowing, + expandFollowing +} from '../../actions/accounts'; +import { ScrollContainer } from 'react-router-scroll'; +import AccountContainer from '../../containers/account_container'; +import Column from '../ui/components/column'; +import HeaderContainer from '../account_timeline/containers/header_container'; +import LoadMore from '../../components/load_more'; +import ColumnBackButton from '../../components/column_back_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const mapStateToProps = (state, props) => ({ + accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']) +}); + +class Following extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + this.handleLoadMore = this.handleLoadMore.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); + this.props.dispatch(fetchFollowing(Number(this.props.params.accountId))); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { + this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); + this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId))); + } + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); + } + } + + handleLoadMore (e) { + e.preventDefault(); + this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); + } + + render () { + const { accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column> + <ColumnBackButton /> + + <ScrollContainer scrollKey='following'> + <div className='scrollable' onScroll={this.handleScroll}> + <div className='following'> + <HeaderContainer accountId={this.props.params.accountId} /> + {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} + <LoadMore onClick={this.handleLoadMore} /> + </div> + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Following.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list +}; + +export default connect(mapStateToProps)(Following); diff --git a/app/javascript/mastodon/features/generic_not_found/index.js b/app/javascript/mastodon/features/generic_not_found/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import Column from '../ui/components/column'; +import MissingIndicator from '../../components/missing_indicator'; + +const GenericNotFound = () => ( + <Column> + <MissingIndicator /> + </Column> +); + +export default GenericNotFound; diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js @@ -0,0 +1,73 @@ +import React from 'react'; +import Column from '../ui/components/column'; +import ColumnLink from '../ui/components/column_link'; +import ColumnSubheading from '../ui/components/column_subheading'; +import { Link } from 'react-router'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, + navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation'}, + settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings'}, + community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local 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: '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' } +}); + +const mapStateToProps = state => ({ + me: state.getIn(['accounts', state.getIn(['meta', 'me'])]) +}); + +class GettingStarted extends ImmutablePureComponent { + + render () { + const { intl, me } = this.props; + + let followRequests = ''; + + if (me.get('locked')) { + followRequests = <ColumnLink icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />; + } + + return ( + <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile={true}> + <div className='getting-started__wrapper'> + <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)}/> + <ColumnLink icon='users' hideOnMobile={true} text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' /> + <ColumnLink icon='globe' hideOnMobile={true} text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' /> + <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> + {followRequests} + <ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' /> + <ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' /> + <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)}/> + <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> + <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> + <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> + </div> + + <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}> + <div className='static-content getting-started'> + <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p> + </div> + </div> + </Column> + ); + } +} + +GettingStarted.propTypes = { + intl: PropTypes.object.isRequired, + me: ImmutablePropTypes.map.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(GettingStarted)); diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../ui/components/column'; +import { + refreshTimeline, + updateTimeline, + deleteFromTimelines +} from '../../actions/timelines'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { FormattedMessage } from 'react-intl'; +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']) +}); + +class HashtagTimeline extends React.PureComponent { + + _subscribe (dispatch, id) { + const { streamingAPIBaseURL, accessToken } = this.props; + + this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, { + + received (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline('tag', JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; + } + } + + }); + } + + _unsubscribe () { + if (typeof this.subscription !== 'undefined') { + this.subscription.close(); + this.subscription = null; + } + } + + componentDidMount () { + const { dispatch } = this.props; + const { id } = this.props.params; + + dispatch(refreshTimeline('tag', id)); + this._subscribe(dispatch, id); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.params.id !== this.props.params.id) { + this.props.dispatch(refreshTimeline('tag', nextProps.params.id)); + this._unsubscribe(); + this._subscribe(this.props.dispatch, nextProps.params.id); + } + } + + componentWillUnmount () { + this._unsubscribe(); + } + + render () { + const { id, hasUnread } = this.props.params; + + return ( + <Column icon='hashtag' active={hasUnread} heading={id}> + <ColumnBackButtonSlim /> + <StatusListContainer scrollKey='hashtag_timeline' type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> + </Column> + ); + } + +} + +HashtagTimeline.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + streamingAPIBaseURL: PropTypes.string.isRequired, + accessToken: PropTypes.string.isRequired, + hasUnread: PropTypes.bool +}; + +export default connect(mapStateToProps)(HashtagTimeline); diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.js b/app/javascript/mastodon/features/home_timeline/components/column_settings.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnCollapsable from '../../../components/column_collapsable'; +import SettingToggle from '../../notifications/components/setting_toggle'; +import SettingText from './setting_text'; + +const messages = defineMessages({ + filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, + settings: { id: 'home.settings', defaultMessage: 'Column settings' } +}); + +class ColumnSettings extends React.PureComponent { + + render () { + const { settings, onChange, onSave, intl } = this.props; + + return ( + <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}> + <div className='column-settings__outer'> + <span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} /> + </div> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} /> + </div> + + <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> + + <div className='column-settings__row'> + <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> + </div> + </div> + </ColumnCollapsable> + ); + } + +} + +ColumnSettings.propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +} + +export default injectIntl(ColumnSettings); diff --git a/app/javascript/mastodon/features/home_timeline/components/setting_text.js b/app/javascript/mastodon/features/home_timeline/components/setting_text.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +class SettingText extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleChange = this.handleChange.bind(this); + } + + handleChange (e) { + this.props.onChange(this.props.settingKey, e.target.value) + } + + render () { + const { settings, settingKey, label } = this.props; + + return ( + <input + className='setting-text' + value={settings.getIn(settingKey)} + onChange={this.handleChange} + placeholder={label} + /> + ); + } + +} + +SettingText.propTypes = { + settings: ImmutablePropTypes.map.isRequired, + settingKey: PropTypes.array.isRequired, + label: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired +}; + +export default SettingText; diff --git a/app/assets/javascripts/components/features/home_timeline/containers/column_settings_container.jsx b/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../ui/components/column'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { Link } from 'react-router'; + +const messages = defineMessages({ + title: { id: 'column.home', defaultMessage: 'Home' } +}); + +const mapStateToProps = state => ({ + hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0 +}); + +class HomeTimeline extends React.PureComponent { + + render () { + const { intl, hasUnread } = this.props; + + return ( + <Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}> + <ColumnSettingsContainer /> + <StatusListContainer {...this.props} scrollKey='home_timeline' type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} /> + </Column> + ); + } + +} + +HomeTimeline.propTypes = { + intl: PropTypes.object.isRequired, + hasUnread: PropTypes.bool +}; + +export default connect(mapStateToProps)(injectIntl(HomeTimeline)); diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +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'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + heading: { id: 'column.mutes', defaultMessage: 'Muted users' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'mutes', 'items']) +}); + +class Mutes extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + } + + 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 ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <ScrollContainer scrollKey='mutes'> + <div className='scrollable mutes' onScroll={this.handleScroll}> + {accountIds.map(id => + <AccountContainer key={id} id={id} /> + )} + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Mutes.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + intl: PropTypes.object.isRequired +}; + +export default connect(mapStateToProps)(injectIntl(Mutes)); diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.js b/app/javascript/mastodon/features/notifications/components/clear_column_button.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } +}); + +class ClearColumnButton extends React.Component { + + render () { + const { intl } = this.props; + + return ( + <div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}> + <i className='fa fa-eraser' /> + </div> + ); + } +} + +ClearColumnButton.propTypes = { + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(ClearColumnButton); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnCollapsable from '../../../components/column_collapsable'; +import SettingToggle from './setting_toggle'; + +const messages = defineMessages({ + settings: { id: 'notifications.settings', defaultMessage: 'Column settings' } +}); + +class ColumnSettings extends React.PureComponent { + + render () { + const { settings, intl, onChange, onSave } = this.props; + + const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; + const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; + const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; + + return ( + <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}> + <div className='column-settings__outer'> + <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> + <SettingToggle settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> + <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> + </div> + + <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> + <SettingToggle settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> + <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> + </div> + + <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> + <SettingToggle settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> + <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> + </div> + + <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> + <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> + <SettingToggle settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> + </div> + </div> + </ColumnCollapsable> + ); + } + +} + +ColumnSettings.propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired + }).isRequired +}; + +export default injectIntl(ColumnSettings); diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js @@ -0,0 +1,90 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import StatusContainer from '../../../containers/status_container'; +import AccountContainer from '../../../containers/account_container'; +import { FormattedMessage } from 'react-intl'; +import Permalink from '../../../components/permalink'; +import emojify from '../../../emoji'; +import escapeTextContentForBrowser from 'escape-html'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class Notification extends ImmutablePureComponent { + + renderFollow (account, link) { + return ( + <div className='notification notification-follow'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-user-plus' /> + </div> + + <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> + </div> + + <AccountContainer id={account.get('id')} withNote={false} /> + </div> + ); + } + + renderMention (notification) { + return <StatusContainer id={notification.get('status')} />; + } + + renderFavourite (notification, link) { + return ( + <div className='notification notification-favourite'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-star star-icon'/> + </div> + + <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> + </div> + + <StatusContainer id={notification.get('status')} muted={true} /> + </div> + ); + } + + renderReblog (notification, link) { + return ( + <div className='notification notification-reblog'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-retweet' /> + </div> + + <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} /> + </div> + + <StatusContainer id={notification.get('status')} muted={true} /> + </div> + ); + } + + 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'); + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; + + switch(notification.get('type')) { + case 'follow': + return this.renderFollow(account, link); + case 'mention': + return this.renderMention(notification); + case 'favourite': + return this.renderFavourite(notification, link); + case 'reblog': + return this.renderReblog(notification, link); + } + } + +} + +Notification.propTypes = { + notification: ImmutablePropTypes.map.isRequired +}; + +export default Notification; diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Toggle from 'react-toggle'; + +const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => ( + <label htmlFor={htmlFor} className='setting-toggle__label'> + <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} /> + <span className='setting-toggle'>{label}</span> + </label> +); + +SettingToggle.propTypes = { + settings: ImmutablePropTypes.map.isRequired, + settingKey: PropTypes.array.isRequired, + label: PropTypes.node.isRequired, + onChange: PropTypes.func.isRequired, + htmlFor: PropTypes.string +}; + +export default SettingToggle; diff --git a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js diff --git a/app/assets/javascripts/components/features/notifications/containers/notification_container.jsx b/app/javascript/mastodon/features/notifications/containers/notification_container.js diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js @@ -0,0 +1,143 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import { expandNotifications, clearNotifications, scrollTopNotifications } from '../../actions/notifications'; +import NotificationContainer from './containers/notification_container'; +import { ScrollContainer } from 'react-router-scroll'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { createSelector } from 'reselect'; +import Immutable from 'immutable'; +import LoadMore from '../../components/load_more'; +import ClearColumnButton from './components/clear_column_button'; +import { openModal } from '../../actions/modal'; + +const messages = defineMessages({ + title: { id: 'column.notifications', defaultMessage: 'Notifications' }, + clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, + clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } +}); + +const getNotifications = createSelector([ + state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), + state => state.getIn(['notifications', 'items']) +], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); + +const mapStateToProps = state => ({ + notifications: getNotifications(state), + isLoading: state.getIn(['notifications', 'isLoading'], true), + isUnread: state.getIn(['notifications', 'unread']) > 0 +}); + +class Notifications extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleScroll = this.handleScroll.bind(this); + this.handleLoadMore = this.handleLoadMore.bind(this); + this.handleClear = this.handleClear.bind(this); + this.setRef = this.setRef.bind(this); + } + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + const offset = scrollHeight - scrollTop - clientHeight; + this._oldScrollPosition = scrollHeight - scrollTop; + + if (250 > offset && !this.props.isLoading) { + this.props.dispatch(expandNotifications()); + } else if (scrollTop < 100) { + this.props.dispatch(scrollTopNotifications(true)); + } else { + this.props.dispatch(scrollTopNotifications(false)); + } + } + + componentDidUpdate (prevProps) { + if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) { + this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition; + } + } + + handleLoadMore (e) { + e.preventDefault(); + this.props.dispatch(expandNotifications()); + } + + handleClear () { + const { dispatch, intl } = this.props; + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(clearNotifications()) + })); + } + + setRef (c) { + this.node = c; + } + + render () { + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread } = this.props; + + let loadMore = ''; + let scrollableArea = ''; + let unread = ''; + + if (!isLoading && notifications.size > 0) { + loadMore = <LoadMore onClick={this.handleLoadMore} />; + } + + if (isUnread) { + unread = <div className='notifications__unread-indicator' />; + } + + if (isLoading || notifications.size > 0) { + scrollableArea = ( + <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}> + {unread} + + <div> + {notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)} + {loadMore} + </div> + </div> + ); + } else { + scrollableArea = ( + <div className='empty-column-indicator' ref={this.setRef}> + <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." /> + </div> + ); + } + + return ( + <Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}> + <ColumnSettingsContainer /> + <ClearColumnButton onClick={this.handleClear} /> + <ScrollContainer scrollKey='notifications' shouldUpdateScroll={shouldUpdateScroll}> + {scrollableArea} + </ScrollContainer> + </Column> + ); + } + +} + +Notifications.propTypes = { + notifications: ImmutablePropTypes.list.isRequired, + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + intl: PropTypes.object.isRequired, + isLoading: PropTypes.bool, + isUnread: PropTypes.bool +}; + +Notifications.defaultProps = { + trackScroll: true +}; + +export default connect(mapStateToProps)(injectIntl(Notifications)); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../ui/components/column'; +import { + refreshTimeline, + updateTimeline, + deleteFromTimelines, + connectTimeline, + disconnectTimeline +} from '../../actions/timelines'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import createStream from '../../stream'; + +const messages = defineMessages({ + title: { id: 'column.public', defaultMessage: 'Federated timeline' } +}); + +const mapStateToProps = state => ({ + hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, + streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), + accessToken: state.getIn(['meta', 'access_token']) +}); + +let subscription; + +class PublicTimeline extends React.PureComponent { + + componentDidMount () { + const { dispatch, streamingAPIBaseURL, accessToken } = this.props; + + dispatch(refreshTimeline('public')); + + if (typeof subscription !== 'undefined') { + return; + } + + subscription = createStream(streamingAPIBaseURL, accessToken, 'public', { + + connected () { + dispatch(connectTimeline('public')); + }, + + reconnected () { + dispatch(connectTimeline('public')); + }, + + disconnected () { + dispatch(disconnectTimeline('public')); + }, + + received (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline('public', JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; + } + } + + }); + } + + componentWillUnmount () { + // if (typeof subscription !== 'undefined') { + // subscription.close(); + // subscription = null; + // } + } + + render () { + const { intl, hasUnread } = this.props; + + return ( + <Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}> + <ColumnBackButtonSlim /> + <StatusListContainer {...this.props} type='public' scrollKey='public_timeline' emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> + </Column> + ); + } + +} + +PublicTimeline.propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + streamingAPIBaseURL: PropTypes.string.isRequired, + accessToken: PropTypes.string.isRequired, + hasUnread: PropTypes.bool +}; + +export default connect(mapStateToProps)(injectIntl(PublicTimeline)); diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { fetchReblogs } from '../../actions/interactions'; +import { ScrollContainer } from 'react-router-scroll'; +import AccountContainer from '../../containers/account_container'; +import Column from '../ui/components/column'; +import ColumnBackButton from '../../components/column_back_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const mapStateToProps = (state, props) => ({ + accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]) +}); + +class Reblogs extends ImmutablePureComponent { + + componentWillMount () { + this.props.dispatch(fetchReblogs(Number(this.props.params.statusId))); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { + this.props.dispatch(fetchReblogs(Number(nextProps.params.statusId))); + } + } + + render () { + const { accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column> + <ColumnBackButton /> + + <ScrollContainer scrollKey='reblogs'> + <div className='scrollable reblogs'> + {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Reblogs.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list +}; + +export default connect(mapStateToProps)(Reblogs); diff --git a/app/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import emojify from '../../../emoji'; +import Toggle from 'react-toggle'; + +class StatusCheckBox extends React.PureComponent { + + render () { + const { status, checked, onToggle, disabled } = this.props; + const content = { __html: emojify(status.get('content')) }; + + if (status.get('reblog')) { + return null; + } + + return ( + <div className='status-check-box'> + <div + className='status__content' + dangerouslySetInnerHTML={content} + /> + + <div className='status-check-box-toggle'> + <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> + </div> + </div> + ); + } + +} + +StatusCheckBox.propTypes = { + status: ImmutablePropTypes.map.isRequired, + checked: PropTypes.bool, + onToggle: PropTypes.func.isRequired, + disabled: PropTypes.bool +}; + +export default StatusCheckBox; diff --git a/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx b/app/javascript/mastodon/features/report/containers/status_check_box_container.js diff --git a/app/javascript/mastodon/features/report/index.js b/app/javascript/mastodon/features/report/index.js @@ -0,0 +1,131 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { cancelReport, changeReportComment, submitReport } from '../../actions/reports'; +import { fetchAccountTimeline } from '../../actions/accounts'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import Button from '../../components/button'; +import { makeGetAccount } from '../../selectors'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import StatusCheckBox from './containers/status_check_box_container'; +import Immutable from 'immutable'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; + +const messages = defineMessages({ + heading: { id: 'report.heading', defaultMessage: 'New report' }, + placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, + submit: { id: 'report.submit', defaultMessage: 'Submit' } +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = state => { + const accountId = state.getIn(['reports', 'new', 'account_id']); + + return { + isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), + account: getAccount(state, accountId), + comment: state.getIn(['reports', 'new', 'comment']), + statusIds: Immutable.OrderedSet(state.getIn(['timelines', 'accounts_timelines', accountId, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])) + }; + }; + + return mapStateToProps; +}; + +class Report extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleCommentChange = this.handleCommentChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentWillMount () { + if (!this.props.account) { + this.context.router.replace('/'); + } + } + + componentDidMount () { + if (!this.props.account) { + return; + } + + this.props.dispatch(fetchAccountTimeline(this.props.account.get('id'))); + } + + componentWillReceiveProps (nextProps) { + if (this.props.account !== nextProps.account && nextProps.account) { + this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id'))); + } + } + + handleCommentChange (e) { + this.props.dispatch(changeReportComment(e.target.value)); + } + + handleSubmit () { + this.props.dispatch(submitReport()); + this.context.router.replace('/'); + } + + render () { + const { account, comment, intl, statusIds, isSubmitting } = this.props; + + if (!account) { + return null; + } + + return ( + <Column heading={intl.formatMessage(messages.heading)} icon='flag'> + <ColumnBackButtonSlim /> + + <div className='report scrollable'> + <div className='report__target'> + <FormattedMessage id='report.target' defaultMessage='Reporting' /> + <strong>{account.get('acct')}</strong> + </div> + + <div className='scrollable report__statuses'> + <div> + {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} + </div> + </div> + + <div className='report__textarea-wrapper'> + <textarea + className='report__textarea' + placeholder={intl.formatMessage(messages.placeholder)} + value={comment} + onChange={this.handleCommentChange} + disabled={isSubmitting} + /> + + <div className='report__submit'> + <div className='report__submit-button'><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div> + </div> + </div> + </div> + </Column> + ); + } + +} + +Report.contextTypes = { + router: PropTypes.object +}; + +Report.propTypes = { + isSubmitting: PropTypes.bool, + account: ImmutablePropTypes.map, + statusIds: ImmutablePropTypes.orderedSet.isRequired, + comment: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default connect(makeMapStateToProps)(injectIntl(Report)); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import IconButton from '../../../components/icon_button'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import DropdownMenu from '../../../components/dropdown_menu'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + report: { id: 'status.report', defaultMessage: 'Report @{name}' } +}); + +class ActionBar extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleReplyClick = this.handleReplyClick.bind(this); + this.handleReblogClick = this.handleReblogClick.bind(this); + this.handleFavouriteClick = this.handleFavouriteClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handleMentionClick = this.handleMentionClick.bind(this); + this.handleReport = this.handleReport.bind(this); + } + + handleReplyClick () { + this.props.onReply(this.props.status); + } + + handleReblogClick (e) { + this.props.onReblog(this.props.status, e); + } + + handleFavouriteClick () { + this.props.onFavourite(this.props.status); + } + + handleDeleteClick () { + this.props.onDelete(this.props.status); + } + + handleMentionClick () { + this.props.onMention(this.props.status.get('account'), this.context.router); + } + + handleReport () { + this.props.onReport(this.props.status); + this.context.router.push('/report'); + } + + render () { + const { status, me, intl } = this.props; + + let menu = []; + + if (me === status.getIn(['account', 'id'])) { + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push(null); + 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'; + + let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); + + return ( + <div className='detailed-status__action-bar'> + <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> + <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> + <div className='detailed-status__button'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> + <div className='detailed-status__button'><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" ariaLabel="More" /></div> + </div> + ); + } + +} + +ActionBar.contextTypes = { + router: PropTypes.object +}; + +ActionBar.propTypes = { + status: ImmutablePropTypes.map.isRequired, + onReply: PropTypes.func.isRequired, + onReblog: PropTypes.func.isRequired, + onFavourite: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, + onReport: PropTypes.func, + me: PropTypes.number.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(ActionBar); diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js @@ -0,0 +1,96 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +const hostStyle = { + display: 'block', + marginTop: '5px', + fontSize: '13px' +}; + +const getHostname = url => { + const parser = document.createElement('a'); + parser.href = url; + return parser.hostname; +}; + +class Card extends React.PureComponent { + + renderLink () { + const { card } = this.props; + + let image = ''; + let provider = card.get('provider_name'); + + if (card.get('image')) { + image = ( + <div className='status-card__image'> + <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' /> + </div> + ); + } + + if (provider.length < 1) { + provider = getHostname(card.get('url')) + } + + return ( + <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'> + {image} + + <div className='status-card__content'> + <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> + <p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p> + <span className='status-card__host' style={hostStyle}>{provider}</span> + </div> + </a> + ); + } + + renderPhoto () { + const { card } = this.props; + + return ( + <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> + <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> + </a> + ); + } + + renderVideo () { + const { card } = this.props; + const content = { __html: card.get('html') }; + + return ( + <div + className='status-card-video' + dangerouslySetInnerHTML={content} + /> + ); + } + + render () { + const { card } = this.props; + + if (card === null) { + return null; + } + + switch(card.get('type')) { + case 'link': + return this.renderLink(); + case 'photo': + return this.renderPhoto(); + case 'video': + return this.renderVideo(); + case 'rich': + default: + return null; + } + } +} + +Card.propTypes = { + card: ImmutablePropTypes.map +}; + +export default Card; diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import StatusContent from '../../../components/status_content'; +import MediaGallery from '../../../components/media_gallery'; +import VideoPlayer from '../../../components/video_player'; +import AttachmentList from '../../../components/attachment_list'; +import { Link } from 'react-router'; +import { FormattedDate, FormattedNumber } from 'react-intl'; +import CardContainer from '../containers/card_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +class DetailedStatus extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleAccountClick = this.handleAccountClick.bind(this); + } + + handleAccountClick (e) { + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + } + + e.stopPropagation(); + } + + render () { + const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + + let media = ''; + let applicationLink = ''; + + if (status.get('media_attachments').size > 0) { + if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { + media = <AttachmentList media={status.get('media_attachments')} />; + } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />; + } else { + media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />; + } + } else if (status.get('spoiler_text').length === 0) { + media = <CardContainer statusId={status.get('id')} />; + } + + if (status.get('application')) { + applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; + } + + return ( + <div className='detailed-status'> + <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> + <div className='detailed-status__display-avatar'><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> + <DisplayName account={status.get('account')} /> + </a> + + <StatusContent status={status} /> + + {media} + + <div className='detailed-status__meta'> + <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> + <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> + </a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> + <i className='fa fa-retweet' /> + <span className='detailed-status__reblogs'> + <FormattedNumber value={status.get('reblogs_count')} /> + </span> + </Link> · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> + <i className='fa fa-star' /> + <span className='detailed-status__favorites'> + <FormattedNumber value={status.get('favourites_count')} /> + </span> + </Link> + </div> + </div> + ); + } + +} + +DetailedStatus.contextTypes = { + router: PropTypes.object +}; + +DetailedStatus.propTypes = { + status: ImmutablePropTypes.map.isRequired, + onOpenMedia: PropTypes.func.isRequired, + onOpenVideo: PropTypes.func.isRequired, + autoPlayGif: PropTypes.bool, +}; + +export default DetailedStatus; diff --git a/app/assets/javascripts/components/features/status/containers/card_container.jsx b/app/javascript/mastodon/features/status/containers/card_container.js diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js @@ -0,0 +1,199 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { fetchStatus } from '../../actions/statuses'; +import Immutable from 'immutable'; +import EmbeddedStatus from '../../components/status'; +import MissingIndicator from '../../components/missing_indicator'; +import DetailedStatus from './components/detailed_status'; +import ActionBar from './components/action_bar'; +import Column from '../ui/components/column'; +import { + favourite, + unfavourite, + reblog, + unreblog +} from '../../actions/interactions'; +import { + replyCompose, + mentionCompose +} from '../../actions/compose'; +import { deleteStatus } from '../../actions/statuses'; +import { initReport } from '../../actions/reports'; +import { + makeGetStatus, + getStatusAncestors, + getStatusDescendants +} from '../../selectors'; +import { ScrollContainer } from 'react-router-scroll'; +import ColumnBackButton from '../../components/column_back_button'; +import StatusContainer from '../../containers/status_container'; +import { openModal } from '../../actions/modal'; +import { isMobile } from '../../is_mobile' +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, + deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' } +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => ({ + status: getStatus(state, Number(props.params.statusId)), + ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), + descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), + me: state.getIn(['meta', 'me']), + boostModal: state.getIn(['meta', 'boost_modal']), + autoPlayGif: state.getIn(['meta', 'auto_play_gif']) + }); + + return mapStateToProps; +}; + +class Status extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleFavouriteClick = this.handleFavouriteClick.bind(this); + this.handleReplyClick = this.handleReplyClick.bind(this); + this.handleModalReblog = this.handleModalReblog.bind(this); + this.handleReblogClick = this.handleReblogClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handleMentionClick = this.handleMentionClick.bind(this); + this.handleOpenMedia = this.handleOpenMedia.bind(this); + this.handleOpenVideo = this.handleOpenVideo.bind(this); + this.handleReport = this.handleReport.bind(this); + } + + componentWillMount () { + this.props.dispatch(fetchStatus(Number(this.props.params.statusId))); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { + this.props.dispatch(fetchStatus(Number(nextProps.params.statusId))); + } + } + + handleFavouriteClick (status) { + if (status.get('favourited')) { + this.props.dispatch(unfavourite(status)); + } else { + this.props.dispatch(favourite(status)); + } + } + + handleReplyClick (status) { + this.props.dispatch(replyCompose(status, this.context.router)); + } + + handleModalReblog (status) { + this.props.dispatch(reblog(status)); + } + + handleReblogClick (status, e) { + if (status.get('reblogged')) { + this.props.dispatch(unreblog(status)); + } else { + if (e.shiftKey || !this.props.boostModal) { + this.handleModalReblog(status); + } else { + this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); + } + } + } + + handleDeleteClick (status) { + const { dispatch, intl } = this.props; + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'))) + })); + } + + handleMentionClick (account, router) { + this.props.dispatch(mentionCompose(account, router)); + } + + handleOpenMedia (media, index) { + this.props.dispatch(openModal('MEDIA', { media, index })); + } + + handleOpenVideo (media, time) { + this.props.dispatch(openModal('VIDEO', { media, time })); + } + + handleReport (status) { + this.props.dispatch(initReport(status.get('account'), status)); + } + + renderChildren (list) { + return list.map(id => <StatusContainer key={id} id={id} />); + } + + render () { + let ancestors, descendants; + const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props; + + if (status === null) { + return ( + <Column> + <ColumnBackButton /> + <MissingIndicator /> + </Column> + ); + } + + const account = status.get('account'); + + if (ancestorsIds && ancestorsIds.size > 0) { + ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; + } + + if (descendantsIds && descendantsIds.size > 0) { + descendants = <div>{this.renderChildren(descendantsIds)}</div>; + } + + return ( + <Column> + <ColumnBackButton /> + + <ScrollContainer scrollKey='thread'> + <div className='scrollable detailed-status__wrapper'> + {ancestors} + + <DetailedStatus status={status} autoPlayGif={autoPlayGif} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} /> + <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} /> + + {descendants} + </div> + </ScrollContainer> + </Column> + ); + } + +} + +Status.contextTypes = { + router: PropTypes.object +}; + +Status.propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + status: ImmutablePropTypes.map, + ancestorsIds: ImmutablePropTypes.list, + descendantsIds: ImmutablePropTypes.list, + me: PropTypes.number, + boostModal: PropTypes.bool, + autoPlayGif: PropTypes.bool, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(connect(makeMapStateToProps)(Status)); diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -0,0 +1,84 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import IconButton from '../../../components/icon_button'; +import Button from '../../../components/button'; +import StatusContent from '../../../components/status_content'; +import Avatar from '../../../components/avatar'; +import RelativeTimestamp from '../../../components/relative_timestamp'; +import DisplayName from '../../../components/display_name'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + reblog: { id: 'status.reblog', defaultMessage: 'Boost' } +}); + +class BoostModal extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.handleReblog = this.handleReblog.bind(this); + this.handleAccountClick = this.handleAccountClick.bind(this); + } + + handleReblog() { + this.props.onReblog(this.props.status); + this.props.onClose(); + } + + handleAccountClick (e) { + if (e.button === 0) { + e.preventDefault(); + this.props.onClose(); + this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + } + } + + render () { + const { status, intl, onClose } = this.props; + + return ( + <div className='modal-root__modal boost-modal'> + <div className='boost-modal__container'> + <div className='status light'> + <div className='boost-modal__status-header'> + <div className='boost-modal__status-time'> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + </div> + + <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> + <div className='status__avatar'> + <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> + </div> + + <DisplayName account={status.get('account')} /> + </a> + </div> + + <StatusContent status={status} /> + </div> + </div> + + <div className='boost-modal__action-bar'> + <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> + <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} /> + </div> + </div> + ); + } + +} + +BoostModal.contextTypes = { + router: PropTypes.object +}; + +BoostModal.propTypes = { + status: ImmutablePropTypes.map.isRequired, + onReblog: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(BoostModal); diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js @@ -0,0 +1,93 @@ +import React from 'react'; +import ColumnHeader from './column_header'; +import PropTypes from 'prop-types'; + +const easingOutQuint = (x, t, b, c, d) => c*((t=t/d-1)*t*t*t*t + 1) + b; + +const scrollTop = (node) => { + const startTime = Date.now(); + const offset = node.scrollTop; + const targetY = -offset; + const duration = 1000; + let interrupt = false; + + const step = () => { + const elapsed = Date.now() - startTime; + const percentage = elapsed / duration; + + if (percentage > 1 || interrupt) { + return; + } + + node.scrollTop = easingOutQuint(0, elapsed, offset, targetY, duration); + requestAnimationFrame(step); + }; + + step(); + + return () => { + interrupt = true; + }; +}; + +class Column extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleHeaderClick = this.handleHeaderClick.bind(this); + this.handleWheel = this.handleWheel.bind(this); + this.setRef = this.setRef.bind(this); + } + + handleHeaderClick () { + const scrollable = this.node.querySelector('.scrollable'); + if (!scrollable) { + return; + } + this._interruptScrollAnimation = scrollTop(scrollable); + } + + handleWheel () { + if (typeof this._interruptScrollAnimation !== 'undefined') { + this._interruptScrollAnimation(); + } + } + + setRef (c) { + this.node = c; + } + + render () { + const { heading, icon, children, active, hideHeadingOnMobile } = this.props; + + let columnHeaderId = null + let header = ''; + + if (heading) { + columnHeaderId = heading.replace(/ /g, '-') + header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId}/>; + } + return ( + <div + ref={this.setRef} + role='region' + aria-labelledby={columnHeaderId} + className='column' + onWheel={this.handleWheel}> + {header} + {children} + </div> + ); + } + +} + +Column.propTypes = { + heading: PropTypes.string, + icon: PropTypes.string, + children: PropTypes.node, + active: PropTypes.bool, + hideHeadingOnMobile: PropTypes.bool +}; + +export default Column; diff --git a/app/javascript/mastodon/features/ui/components/column_header.js b/app/javascript/mastodon/features/ui/components/column_header.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types' + +class ColumnHeader extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + } + + handleClick () { + this.props.onClick(); + } + + render () { + const { type, active, hideOnMobile, columnHeaderId } = this.props; + + let icon = ''; + + if (this.props.icon) { + icon = <i className={`fa fa-fw fa-${this.props.icon} column-header__icon`} />; + } + + return ( + <div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> + {icon} + {type} + </div> + ); + } + +} + +ColumnHeader.propTypes = { + icon: PropTypes.string, + type: PropTypes.string, + active: PropTypes.bool, + onClick: PropTypes.func, + hideOnMobile: PropTypes.bool, + columnHeaderId: PropTypes.string +}; + +export default ColumnHeader; diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router'; + +const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => { + if (href) { + return ( + <a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> + <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + {text} + </a> + ); + } else { + return ( + <Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}> + <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + {text} + </Link> + ); + } +}; + +ColumnLink.propTypes = { + icon: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + to: PropTypes.string, + href: PropTypes.string, + method: PropTypes.string, + hideOnMobile: PropTypes.bool +}; + +export default ColumnLink; diff --git a/app/javascript/mastodon/features/ui/components/column_subheading.js b/app/javascript/mastodon/features/ui/components/column_subheading.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ColumnSubheading = ({ text }) => { + return ( + <div className='column-subheading'> + {text} + </div> + ); +}; + +ColumnSubheading.propTypes = { + text: PropTypes.string.isRequired, +}; + +export default ColumnSubheading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class ColumnsArea extends React.PureComponent { + + render () { + return ( + <div className='columns-area'> + {this.props.children} + </div> + ); + } + +} + +ColumnsArea.propTypes = { + children: PropTypes.node +}; + +export default ColumnsArea; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.js b/app/javascript/mastodon/features/ui/components/confirmation_modal.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Button from '../../../components/button'; + +class ConfirmationModal extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + this.handleCancel = this.handleCancel.bind(this); + } + + handleClick () { + this.props.onClose(); + this.props.onConfirm(); + } + + handleCancel (e) { + e.preventDefault(); + this.props.onClose(); + } + + render () { + const { intl, message, confirm, onConfirm, onClose } = this.props; + + return ( + <div className='modal-root__modal confirmation-modal'> + <div className='confirmation-modal__container'> + {message} + </div> + + <div className='confirmation-modal__action-bar'> + <div><a href='#' onClick={this.handleCancel}><FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /></a></div> + <Button text={confirm} onClick={this.handleClick} /> + </div> + </div> + ); + } + +} + +ConfirmationModal.propTypes = { + message: PropTypes.node.isRequired, + confirm: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(ConfirmationModal); diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -0,0 +1,103 @@ +import React from 'react'; +import LoadingIndicator from '../../../components/loading_indicator'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import ImageLoader from 'react-imageloader'; +import { defineMessages, injectIntl } from 'react-intl'; +import IconButton from '../../../components/icon_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' } +}); + +class MediaModal extends ImmutablePureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + index: null + }; + this.handleNextClick = this.handleNextClick.bind(this); + this.handlePrevClick = this.handlePrevClick.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + } + + handleNextClick () { + this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); + } + + handlePrevClick () { + this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); + } + + handleKeyUp (e) { + switch(e.key) { + case 'ArrowLeft': + this.handlePrevClick(); + break; + case 'ArrowRight': + this.handleNextClick(); + break; + } + } + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + } + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + } + + getIndex () { + return this.state.index !== null ? this.state.index : this.props.index; + } + + render () { + const { media, intl, onClose } = this.props; + + const index = this.getIndex(); + const attachment = media.get(index); + const url = attachment.get('url'); + + let leftNav, rightNav, content; + + leftNav = rightNav = content = ''; + + if (media.size > 1) { + leftNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; + rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; + } + + if (attachment.get('type') === 'image') { + content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />; + } else if (attachment.get('type') === 'gifv') { + content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />; + } + + return ( + <div className='modal-root__modal media-modal'> + {leftNav} + + <div className='media-modal__content'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> + {content} + </div> + + {rightNav} + </div> + ); + } + +} + +MediaModal.propTypes = { + media: ImmutablePropTypes.list.isRequired, + index: PropTypes.number.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(MediaModal); diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import MediaModal from './media_modal'; +import OnboardingModal from './onboarding_modal'; +import VideoModal from './video_modal'; +import BoostModal from './boost_modal'; +import ConfirmationModal from './confirmation_modal'; +import { TransitionMotion, spring } from 'react-motion'; + +const MODAL_COMPONENTS = { + 'MEDIA': MediaModal, + 'ONBOARDING': OnboardingModal, + 'VIDEO': VideoModal, + 'BOOST': BoostModal, + 'CONFIRM': ConfirmationModal +}; + +class ModalRoot extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.handleKeyUp = this.handleKeyUp.bind(this); + } + + handleKeyUp (e) { + if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) + && !!this.props.type) { + this.props.onClose(); + } + } + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + } + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + } + + willEnter () { + return { opacity: 0, scale: 0.98 }; + } + + willLeave () { + return { opacity: spring(0), scale: spring(0.98) }; + } + + render () { + const { type, props, onClose } = this.props; + const items = []; + + if (!!type) { + items.push({ + key: type, + data: { type, props }, + style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } + }); + } + + return ( + <TransitionMotion + styles={items} + willEnter={this.willEnter} + willLeave={this.willLeave}> + {interpolatedStyles => + <div className='modal-root'> + {interpolatedStyles.map(({ key, data: { type, props }, style }) => { + const SpecificComponent = MODAL_COMPONENTS[type]; + + return ( + <div key={key}> + <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> + <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> + <SpecificComponent {...props} onClose={onClose} /> + </div> + </div> + ); + })} + </div> + } + </TransitionMotion> + ); + } + +} + +ModalRoot.propTypes = { + type: PropTypes.string, + props: PropTypes.object, + onClose: PropTypes.func.isRequired +}; + +export default ModalRoot; diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -0,0 +1,264 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Permalink from '../../../components/permalink'; +import { TransitionMotion, spring } from 'react-motion'; +import ComposeForm from '../../compose/components/compose_form'; +import Search from '../../compose/components/search'; +import NavigationBar from '../../compose/components/navigation_bar'; +import ColumnHeader from './column_header'; +import Immutable from 'immutable'; + +const messages = defineMessages({ + home_title: { id: 'column.home', defaultMessage: 'Home' }, + notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, + local_title: { id: 'column.community', defaultMessage: 'Local timeline' }, + federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' } +}); + +const PageOne = ({ acct, domain }) => ( + <div className='onboarding-modal__page onboarding-modal__page-one'> + <div style={{ flex: '0 0 auto' }}> + <div className='onboarding-modal__page-one__elephant-friend' /> + </div> + + <div> + <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1> + <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' /></p> + <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p> + </div> + </div> +); + +PageOne.propTypes = { + acct: PropTypes.string.isRequired, + domain: PropTypes.string.isRequired +}; + +const PageTwo = ({ me }) => ( + <div className='onboarding-modal__page onboarding-modal__page-two'> + <div className='figure non-interactive'> + <div className='pseudo-drawer'> + <NavigationBar account={me} /> + </div> + <ComposeForm + text='Awoo! #introductions' + suggestions={Immutable.List()} + mentionedDomains={[]} + spoiler={false} + onChange={() => {}} + onSubmit={() => {}} + onPaste={() => {}} + onPickEmoji={() => {}} + onChangeSpoilerText={() => {}} + onClearSuggestions={() => {}} + onFetchSuggestions={() => {}} + onSuggestionSelected={() => {}} + /> + </div> + + <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p> + </div> +); + +PageTwo.propTypes = { + me: ImmutablePropTypes.map.isRequired, +}; + +const PageThree = ({ me, domain }) => ( + <div className='onboarding-modal__page onboarding-modal__page-three'> + <div className='figure non-interactive'> + <Search + value='' + onChange={() => {}} + onSubmit={() => {}} + onClear={() => {}} + onShow={() => {}} + /> + + <div className='pseudo-drawer'> + <NavigationBar account={me} /> + </div> + </div> + + <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p> + <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p> + </div> +); + +PageThree.propTypes = { + me: ImmutablePropTypes.map.isRequired, + domain: PropTypes.string.isRequired +}; + +const PageFour = ({ domain, intl }) => ( + <div className='onboarding-modal__page onboarding-modal__page-four'> + <div className='onboarding-modal__page-four__columns'> + <div className='row'> + <div> + <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div> + <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.'/></p> + </div> + + <div> + <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div> + <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' /></p> + </div> + </div> + + <div className='row'> + <div> + <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div> + </div> + + <div> + <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div> + </div> + </div> + + <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} /></p> + </div> + </div> +); + +PageFour.propTypes = { + domain: PropTypes.string.isRequired, + intl: PropTypes.object.isRequired +}; + +const PageSix = ({ admin, domain }) => { + let adminSection = ''; + + if (admin) { + adminSection = ( + <p> + <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} /> + <br /> + <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/> + </p> + ); + } + + return ( + <div className='onboarding-modal__page onboarding-modal__page-six'> + <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1> + {adminSection} + <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> + <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> + <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p> + </div> + ); +}; + +PageSix.propTypes = { + admin: ImmutablePropTypes.map, + domain: PropTypes.string.isRequired +}; + +const mapStateToProps = state => ({ + me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), + admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), + domain: state.getIn(['meta', 'domain']) +}); + +class OnboardingModal extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + currentIndex: 0 + }; + this.handleSkip = this.handleSkip.bind(this); + this.handleDot = this.handleDot.bind(this); + this.handleNext = this.handleNext.bind(this); + } + + handleSkip (e) { + e.preventDefault(); + this.props.onClose(); + } + + handleDot (i, e) { + e.preventDefault(); + this.setState({ currentIndex: i }); + } + + handleNext (maxNum, e) { + e.preventDefault(); + + if (this.state.currentIndex < maxNum - 1) { + this.setState({ currentIndex: this.state.currentIndex + 1 }); + } else { + this.props.onClose(); + } + } + + render () { + const { me, admin, domain, intl } = this.props; + + const pages = [ + <PageOne acct={me.get('acct')} domain={domain} />, + <PageTwo me={me} />, + <PageThree me={me} domain={domain} />, + <PageFour domain={domain} intl={intl} />, + <PageSix admin={admin} domain={domain} /> + ]; + + const { currentIndex } = this.state; + const hasMore = currentIndex < pages.length - 1; + + let nextOrDoneBtn; + + if(hasMore) { + nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>; + } else { + nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.done' defaultMessage='Done' /></a>; + } + + const styles = pages.map((page, i) => ({ + key: `page-${i}`, + style: { opacity: spring(i === currentIndex ? 1 : 0) } + })); + + return ( + <div className='modal-root__modal onboarding-modal'> + <TransitionMotion styles={styles}> + {interpolatedStyles => + <div className='onboarding-modal__pager'> + {pages.map((page, i) => + <div key={`page-${i}`} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div> + )} + </div> + } + </TransitionMotion> + + <div className='onboarding-modal__paginator'> + <div> + <a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a> + </div> + + <div className='onboarding-modal__dots'> + {pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)} + </div> + + <div> + {nextOrDoneBtn} + </div> + </div> + </div> + ); + } + +} + +OnboardingModal.propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + me: ImmutablePropTypes.map.isRequired, + domain: PropTypes.string.isRequired, + admin: ImmutablePropTypes.map +} + +export default connect(mapStateToProps)(injectIntl(OnboardingModal)); diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Link } from 'react-router'; +import { FormattedMessage } from 'react-intl'; + +class TabsBar extends React.Component { + + render () { + return ( + <div className='tabs-bar'> + <Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link> + <Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link> + <Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link> + + <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link> + <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link> + + <Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link> + </div> + ); + } + +} + +export default TabsBar; diff --git a/app/javascript/mastodon/features/ui/components/upload_area.js b/app/javascript/mastodon/features/ui/components/upload_area.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Motion, spring } from 'react-motion'; +import { FormattedMessage } from 'react-intl'; + +class UploadArea extends React.PureComponent { + + constructor (props, context) { + super(props, context); + + this.handleKeyUp = this.handleKeyUp.bind(this); + } + + handleKeyUp (e) { + e.preventDefault(); + e.stopPropagation(); + + const keyCode = e.keyCode + if (this.props.active) { + switch(keyCode) { + case 27: + this.props.onClose(); + break; + } + } + } + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + } + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + } + + render () { + const { active } = this.props; + + return ( + <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}> + {({ backgroundOpacity, backgroundScale }) => + <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> + <div className='upload-area__drop'> + <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} /> + <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> + </div> + </div> + } + </Motion> + ); + } + +} + +UploadArea.propTypes = { + active: PropTypes.bool, + onClose: PropTypes.func +}; + +export default UploadArea; diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -0,0 +1,40 @@ +import React from 'react'; +import LoadingIndicator from '../../../components/loading_indicator'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import { defineMessages, injectIntl } from 'react-intl'; +import IconButton from '../../../components/icon_button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' } +}); + +class VideoModal extends ImmutablePureComponent { + + render () { + const { media, intl, time, onClose } = this.props; + + const url = media.get('url'); + + return ( + <div className='modal-root__modal media-modal'> + <div> + <div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div> + <ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} /> + </div> + </div> + ); + } + +} + +VideoModal.propTypes = { + media: ImmutablePropTypes.map.isRequired, + time: PropTypes.number, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default injectIntl(VideoModal); diff --git a/app/assets/javascripts/components/features/ui/containers/loading_bar_container.jsx b/app/javascript/mastodon/features/ui/containers/loading_bar_container.js diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/javascript/mastodon/features/ui/containers/modal_container.js diff --git a/app/assets/javascripts/components/features/ui/containers/notifications_container.jsx b/app/javascript/mastodon/features/ui/containers/notifications_container.js diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/javascript/mastodon/features/ui/containers/status_list_container.js diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js @@ -0,0 +1,169 @@ +import React from 'react'; +import ColumnsArea from './components/columns_area'; +import NotificationsContainer from './containers/notifications_container'; +import PropTypes from 'prop-types'; +import LoadingBarContainer from './containers/loading_bar_container'; +import HomeTimeline from '../home_timeline'; +import Compose from '../compose'; +import TabsBar from './components/tabs_bar'; +import ModalContainer from './containers/modal_container'; +import Notifications from '../notifications'; +import { connect } from 'react-redux'; +import { isMobile } from '../../is_mobile'; +import { debounce } from 'react-decoration'; +import { uploadCompose } from '../../actions/compose'; +import { refreshTimeline } from '../../actions/timelines'; +import { refreshNotifications } from '../../actions/notifications'; +import UploadArea from './components/upload_area'; + +const noOp = () => false; + +class UI extends React.PureComponent { + + constructor (props, context) { + super(props, context); + this.state = { + width: window.innerWidth, + draggingOver: false + }; + this.handleResize = this.handleResize.bind(this); + this.handleDragEnter = this.handleDragEnter.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.handleDrop = this.handleDrop.bind(this); + this.handleDragLeave = this.handleDragLeave.bind(this); + this.handleDragEnd = this.handleDragLeave.bind(this) + this.closeUploadModal = this.closeUploadModal.bind(this) + this.setRef = this.setRef.bind(this); + } + + @debounce(500) + handleResize () { + this.setState({ width: window.innerWidth }); + } + + handleDragEnter (e) { + e.preventDefault(); + + if (!this.dragTargets) { + this.dragTargets = []; + } + + if (this.dragTargets.indexOf(e.target) === -1) { + this.dragTargets.push(e.target); + } + + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + this.setState({ draggingOver: true }); + } + } + + handleDragOver (e) { + e.preventDefault(); + e.stopPropagation(); + + try { + e.dataTransfer.dropEffect = 'copy'; + } catch (err) { + + } + + return false; + } + + handleDrop (e) { + e.preventDefault(); + + this.setState({ draggingOver: false }); + + if (e.dataTransfer && e.dataTransfer.files.length === 1) { + this.props.dispatch(uploadCompose(e.dataTransfer.files)); + } + } + + handleDragLeave (e) { + e.preventDefault(); + e.stopPropagation(); + + this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); + + if (this.dragTargets.length > 0) { + return; + } + + this.setState({ draggingOver: false }); + } + + closeUploadModal() { + this.setState({ draggingOver: false }); + } + + componentWillMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + document.addEventListener('dragenter', this.handleDragEnter, false); + document.addEventListener('dragover', this.handleDragOver, false); + document.addEventListener('drop', this.handleDrop, false); + document.addEventListener('dragleave', this.handleDragLeave, false); + document.addEventListener('dragend', this.handleDragEnd, false); + + this.props.dispatch(refreshTimeline('home')); + this.props.dispatch(refreshNotifications()); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + document.removeEventListener('dragenter', this.handleDragEnter); + document.removeEventListener('dragover', this.handleDragOver); + document.removeEventListener('drop', this.handleDrop); + document.removeEventListener('dragleave', this.handleDragLeave); + document.removeEventListener('dragend', this.handleDragEnd); + } + + setRef (c) { + this.node = c; + } + + render () { + const { width, draggingOver } = this.state; + const { children } = this.props; + + let mountedColumns; + + if (isMobile(width)) { + mountedColumns = ( + <ColumnsArea> + {children} + </ColumnsArea> + ); + } else { + mountedColumns = ( + <ColumnsArea> + <Compose withHeader={true} /> + <HomeTimeline shouldUpdateScroll={noOp} /> + <Notifications shouldUpdateScroll={noOp} /> + <div style={{display: 'flex', flex: '1 1 auto', position: 'relative'}}>{children}</div> + </ColumnsArea> + ); + } + + return ( + <div className='ui' ref={this.setRef}> + <TabsBar /> + + {mountedColumns} + + <NotificationsContainer /> + <LoadingBarContainer className="loading-bar" /> + <ModalContainer /> + <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> + </div> + ); + } + +} + +UI.propTypes = { + dispatch: PropTypes.func.isRequired, + children: PropTypes.node +}; + +export default connect()(UI); diff --git a/app/assets/javascripts/components/is_mobile.jsx b/app/javascript/mastodon/is_mobile.js diff --git a/app/assets/javascripts/components/link_header.jsx b/app/javascript/mastodon/link_header.js diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json @@ -0,0 +1,172 @@ +{ + "account.block": "حظر @{name}", + "account.disclaimer": "هذا المستخدم من مثيل خادم آخر. قد يكون هذا الرقم أكبر.", + "account.edit_profile": "تعديل الملف الشخصي", + "account.follow": "إتبع", + "account.followers": "المتابعون", + "account.follows": "يتبع", + "account.follows_you": "يتابعك", + "account.mention": "أُذكُر @{name}", + "account.mute": "أكتم @{name}", + "account.posts": "المشاركات", + "account.report": "أبلغ عن @{name}", + "account.requested": "في انتظار الموافقة", + "account.unblock": "إلغاء الحظر عن @{name}", + "account.unfollow": "إلغاء المتابعة", + "account.unmute": "إلغاء الكتم عن @{name}", + "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة", + "column.blocks": "الحسابات المحجوبة", + "column.community": "الخيط العام المحلي", + "column.favourites": "المفضلة", + "column.follow_requests": "طلبات المتابعة", + "column.home": "الرئيسية", + "column.mutes": "الحسابات المكتومة", + "column.notifications": "الإشعارات", + "column.public": "الخيط العام الموحد", + "column_back_button.label": "العودة", + "column_subheading.navigation": "التصفح", + "column_subheading.settings": "الإعدادات", + "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.", + "compose_form.lock_disclaimer.lock": "مقفل", + "compose_form.placeholder": "فيمَ تفكّر؟", + "compose_form.publish": "بوّق !", + "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس", + "compose_form.spoiler": "أخفِ النص واعرض تحذيرا", + "compose_form.spoiler_placeholder": "تنبيه عن المحتوى", + "confirmation_modal.cancel": "إلغاء", + "confirmations.block.confirm": "حجب", + "confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟", + "confirmations.delete.confirm": "حذف", + "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟", + "confirmations.mute.confirm": "أكتم", + "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", + "emoji_button.activity": "الأنشطة", + "emoji_button.flags": "الأعلام", + "emoji_button.food": "الطعام والشراب", + "emoji_button.label": "أدرج إيموجي", + "emoji_button.nature": "الطبيعة", + "emoji_button.objects": "أشياء", + "emoji_button.people": "الناس", + "emoji_button.search": "ابحث...", + "emoji_button.symbols": "رموز", + "emoji_button.travel": "أماكن و أسفار", + "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.", + "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.", + "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.", + "empty_column.home.public_timeline": "الخيط العام", + "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", + "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.", + "follow_request.authorize": "ترخيص", + "follow_request.reject": "رفض", + "getting_started.apps": "عدة تطبيقات مختلفة متوفرة", + "getting_started.heading": "إستعدّ للبدء", + "getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على GitHub {github}. {apps}.", + "home.column_settings.advanced": "متقدمة", + "home.column_settings.basic": "أساسية", + "home.column_settings.filter_regex": "تصفية حسب التعبيرات العادية", + "home.column_settings.show_reblogs": "عرض الترقيات", + "home.column_settings.show_replies": "عرض الردود", + "home.settings": "إعدادات العمود", + "lightbox.close": "إغلاق", + "loading_indicator.label": "تحميل ...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "تعذر العثور عليه", + "navigation_bar.blocks": "الحسابات المحجوبة", + "navigation_bar.community_timeline": "الخيط العام المحلي", + "navigation_bar.edit_profile": "تعديل الملف الشخصي", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "خروج", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "التفضيلات", + "navigation_bar.public_timeline": "الخيط العام الموحد", + "notification.favourite": "{name} أعجب بمنشورك", + "notification.follow": "{name} يتبعك", + "notification.reblog": "{name} قام بترقية تبويقك", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "إشعارات سطح المكتب", + "notifications.column_settings.favourite": "المُفَضَّلة :", + "notifications.column_settings.follow": "متابعُون جُدُد :", + "notifications.column_settings.mention": "الإشارات :", + "notifications.column_settings.reblog": "الترقيّات:", + "notifications.column_settings.show": "إعرِضها في عمود", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "إلغاء", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "ابحث", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "إحذف", + "status.favourite": "أضف إلى المفضلة", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "أذكُر @{name}", + "status.open": "وسع هذه المشاركة", + "status.reblog": "رَقِّي", + "status.reblogged_by": "{name} رقى", + "status.reply": "ردّ", + "status.replyAll": "Reply to thread", + "status.report": "إبلِغ عن @{name}", + "status.sensitive_toggle": "اضغط للعرض", + "status.sensitive_warning": "محتوى حساس", + "status.show_less": "إعرض أقلّ", + "status.show_more": "أظهر المزيد", + "tabs_bar.compose": "تحرير", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "الرئيسية", + "tabs_bar.mentions": "الإشارات", + "tabs_bar.public": "الخيط العام الموحد", + "tabs_bar.notifications": "الإخطارات", + "upload_button.label": "إضافة وسائط", + "upload_form.undo": "إلغاء", + "upload_progress.label": "يرفع...", + "notification.follow": "{name} يتبعك", + "notification.favourite": "{name} أعجب بمنشورك", + "notification.reblog": "{name} قام بترقية تبويقك", + "notification.mention": "{name} ذكرك", + "notifications.column_settings.alert": "إشعارات سطح المكتب", + "notifications.column_settings.show": "إعرِضها في عمود", + "notifications.column_settings.follow": "متابعُون جُدُد :", + "notifications.column_settings.favourite": "المُفَضَّلة :", + "notifications.column_settings.mention": "الإشارات :", + "notifications.column_settings.reblog": "الترقيّات:", + "video_player.toggle_sound": "تبديل الصوت", + "video_player.toggle_visible": "إظهار / إخفاء الفيديو", + "video_player.expand": "وسّع الفيديو", + "video_player.video_error": "تعذر تشغيل الفيديو" +} diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json @@ -0,0 +1,163 @@ +{ + "account.block": "Блокирай", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Редактирай профила си", + "account.follow": "Последвай", + "account.followers": "Последователи", + "account.follows": "Следвам", + "account.follows_you": "Твой последовател", + "account.mention": "Споменаване", + "account.mute": "Mute @{name}", + "account.posts": "Публикации", + "account.report": "Report @{name}", + "account.requested": "В очакване на одобрение", + "account.unblock": "Не блокирай", + "account.unfollow": "Не следвай", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Начало", + "column.mutes": "Muted users", + "column.notifications": "Известия", + "column.public": "Публичен канал", + "column_back_button.label": "Назад", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Какво си мислиш?", + "compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?", + "compose_form.publish": "Раздумай", + "compose_form.sensitive": "Отбележи съдържанието като деликатно", + "compose_form.spoiler": "Скрий текста зад предупреждение", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Първи стъпки", + "getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Затвори", + "loading_indicator.label": "Зареждане...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.edit_profile": "Редактирай профил", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Излизане", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Предпочитания", + "navigation_bar.public_timeline": "Публичен канал", + "notification.favourite": "{name} хареса твоята публикация", + "notification.follow": "{name} те последва", + "notification.reblog": "{name} сподели твоята публикация", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Десктоп известия", + "notifications.column_settings.favourite": "Предпочитани:", + "notifications.column_settings.follow": "Нови последователи:", + "notifications.column_settings.mention": "Споменавания:", + "notifications.column_settings.reblog": "Споделяния:", + "notifications.column_settings.show": "Покажи в колона", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Отказ", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Търсене", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Изтриване", + "status.favourite": "Предпочитани", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Споменаване", + "status.open": "Expand this status", + "status.reblog": "Споделяне", + "status.reblogged_by": "{name} сподели", + "status.reply": "Отговор", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Покажи", + "status.sensitive_warning": "Деликатно съдържание", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Съставяне", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Начало", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Известия", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Добави медия", + "upload_form.undo": "Отмяна", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Звук", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json @@ -0,0 +1,163 @@ +{ + "account.block": "@{name} blocken", + "account.disclaimer": "Dieser Benutzer ist von einer anderen Instanz. Diese Zahl könnte größer sein.", + "account.edit_profile": "Profil bearbeiten", + "account.follow": "Folgen", + "account.followers": "Folgende", + "account.follows": "Folgt", + "account.follows_you": "Folgt dir", + "account.mention": "@{name} erwähnen", + "account.mute": "@{name} stummschalten", + "account.posts": "Beiträge", + "account.report": "@{name} melden", + "account.requested": "Warte auf Erlaubnis", + "account.unblock": "@{name} entblocken", + "account.unfollow": "Entfolgen", + "account.unmute": "@{name} nicht mehr stummschalten", + "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen", + "column.blocks": "Blockierte Benutzer", + "column.community": "Lokale Zeitleiste", + "column.favourites": "Favoriten", + "column.follow_requests": "Folgeanfragen", + "column.home": "Startseite", + "column.mutes": "Stummgeschaltete Benutzer", + "column.notifications": "Mitteilungen", + "column.public": "Gesamtes bekanntes Netz", + "column_back_button.label": "Zurück", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Worüber möchtest du schreiben?", + "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.", + "compose_form.publish": "Tröt", + "compose_form.sensitive": "Medien als heikel markieren", + "compose_form.spoiler": "Text hinter Warnung verbergen", + "compose_form.spoiler_placeholder": "Inhaltswarnung", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Emoji einfügen", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", + "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.", + "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.", + "empty_column.home.public_timeline": "die öffentliche Zeitleiste", + "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.", + "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.", + "follow_request.authorize": "Erlauben", + "follow_request.reject": "Ablehnen", + "getting_started.apps": "Es sind verschiedene Apps verfügbar", + "getting_started.heading": "Erste Schritte", + "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", + "home.column_settings.advanced": "Fortgeschritten", + "home.column_settings.basic": "Einfach", + "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke", + "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", + "home.column_settings.show_replies": "Antworten anzeigen", + "home.settings": "Spalteneinstellungen", + "lightbox.close": "Schließen", + "loading_indicator.label": "Lade…", + "media_gallery.toggle_visible": "Sichtbarkeit einstellen", + "missing_indicator.label": "Nicht gefunden", + "navigation_bar.blocks": "Blockierte Benutzer", + "navigation_bar.community_timeline": "Lokale Zeitleiste", + "navigation_bar.edit_profile": "Profil bearbeiten", + "navigation_bar.favourites": "Favoriten", + "navigation_bar.follow_requests": "Folgeanfragen", + "navigation_bar.info": "Erweiterte Informationen", + "navigation_bar.logout": "Abmelden", + "navigation_bar.mutes": "Stummgeschaltete Benutzer", + "navigation_bar.preferences": "Einstellungen", + "navigation_bar.public_timeline": "Föderierte Zeitleiste", + "notification.favourite": "{name} favorisierte deinen Status", + "notification.follow": "{name} folgt dir", + "notification.reblog": "{name} teilte deinen Status", + "notifications.clear": "Mitteilungen beseitigen", + "notifications.clear_confirmation": "Bist du sicher, dass du alle Mitteilungen beseitigen willst?", + "notifications.column_settings.alert": "Desktop-Benachrichtigungen", + "notifications.column_settings.favourite": "Favorisierungen:", + "notifications.column_settings.follow": "Neue Folgende:", + "notifications.column_settings.mention": "Erwähnungen:", + "notifications.column_settings.reblog": "Geteilte Beiträge:", + "notifications.column_settings.show": "In der Spalte anzeigen", + "notifications.column_settings.sound": "Ton abspielen", + "notifications.settings": "Spalteneinstellungen", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Privatsphäre des Status anpassen", + "privacy.direct.long": "Beitrag nur an erwähnte Benutzer", + "privacy.direct.short": "Direkt", + "privacy.private.long": "Beitrag nur an Folgende", + "privacy.private.short": "Privat", + "privacy.public.long": "Beitrag an öffentliche Zeitleisten", + "privacy.public.short": "Öffentlich", + "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen", + "privacy.unlisted.short": "Nicht gelistet", + "reply_indicator.cancel": "Abbrechen", + "report.heading": "Neue Meldung", + "report.placeholder": "Zusätzliche Kommentare", + "report.submit": "Absenden", + "report.target": "Melden", + "search.placeholder": "Suche", + "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Löschen", + "status.favourite": "Favorisieren", + "status.load_more": "Weitere laden", + "status.media_hidden": "Medien versteckt", + "status.mention": "Erwähnen", + "status.open": "Öffnen", + "status.reblog": "Teilen", + "status.reblogged_by": "{name} teilte", + "status.reply": "Antworten", + "status.replyAll": "Auf Thread antworten", + "status.report": "@{name} melden", + "status.sensitive_toggle": "Klicke, um sie zu sehen", + "status.sensitive_warning": "Heikle Inhalte", + "status.show_less": "Weniger anzeigen", + "status.show_more": "Mehr anzeigen", + "tabs_bar.compose": "Schreiben", + "tabs_bar.federated_timeline": "Föderation", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Lokal", + "tabs_bar.notifications": "Mitteilungen", + "upload_area.title": "Hereinziehen zum Hochladen", + "upload_button.label": "Mediendatei hinzufügen", + "upload_form.undo": "Entfernen", + "upload_progress.label": "Lade hoch…", + "video_player.expand": "Videoanzeige vergrößern", + "video_player.toggle_sound": "Ton umschalten", + "video_player.toggle_visible": "Sichtbarkeit umschalten", + "video_player.video_error": "Video konnte nicht abgespielt werden" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json @@ -0,0 +1,1068 @@ +[ + { + "descriptors": [ + { + "defaultMessage": "Follow", + "id": "account.follow" + }, + { + "defaultMessage": "Unfollow", + "id": "account.unfollow" + }, + { + "defaultMessage": "Awaiting approval", + "id": "account.requested" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + } + ], + "path": "app/javascript/mastodon/components/account.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Back", + "id": "column_back_button.label" + } + ], + "path": "app/javascript/mastodon/components/column_back_button_slim.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Back", + "id": "column_back_button.label" + } + ], + "path": "app/javascript/mastodon/components/column_back_button.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Load more", + "id": "status.load_more" + } + ], + "path": "app/javascript/mastodon/components/load_more.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Loading...", + "id": "loading_indicator.label" + } + ], + "path": "app/javascript/mastodon/components/loading_indicator.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Toggle visibility", + "id": "media_gallery.toggle_visible" + }, + { + "defaultMessage": "Sensitive content", + "id": "status.sensitive_warning" + }, + { + "defaultMessage": "Media hidden", + "id": "status.media_hidden" + }, + { + "defaultMessage": "Click to view", + "id": "status.sensitive_toggle" + } + ], + "path": "app/javascript/mastodon/components/media_gallery.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Not found", + "id": "missing_indicator.label" + } + ], + "path": "app/javascript/mastodon/components/missing_indicator.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Delete", + "id": "status.delete" + }, + { + "defaultMessage": "Mention @{name}", + "id": "status.mention" + }, + { + "defaultMessage": "Mute @{name}", + "id": "account.mute" + }, + { + "defaultMessage": "Block @{name}", + "id": "account.block" + }, + { + "defaultMessage": "Reply", + "id": "status.reply" + }, + { + "defaultMessage": "Reply to thread", + "id": "status.replyAll" + }, + { + "defaultMessage": "Boost", + "id": "status.reblog" + }, + { + "defaultMessage": "This post cannot be boosted", + "id": "status.cannot_reblog" + }, + { + "defaultMessage": "Favourite", + "id": "status.favourite" + }, + { + "defaultMessage": "Expand this status", + "id": "status.open" + }, + { + "defaultMessage": "Report @{name}", + "id": "status.report" + } + ], + "path": "app/javascript/mastodon/components/status_action_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Show more", + "id": "status.show_more" + }, + { + "defaultMessage": "Show less", + "id": "status.show_less" + } + ], + "path": "app/javascript/mastodon/components/status_content.json" + }, + { + "descriptors": [ + { + "defaultMessage": "{name} boosted", + "id": "status.reblogged_by" + } + ], + "path": "app/javascript/mastodon/components/status.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Toggle sound", + "id": "video_player.toggle_sound" + }, + { + "defaultMessage": "Toggle visibility", + "id": "video_player.toggle_visible" + }, + { + "defaultMessage": "Expand video", + "id": "video_player.expand" + }, + { + "defaultMessage": "Video could not be played", + "id": "video_player.video_error" + }, + { + "defaultMessage": "Sensitive content", + "id": "status.sensitive_warning" + }, + { + "defaultMessage": "Click to view", + "id": "status.sensitive_toggle" + }, + { + "defaultMessage": "Media hidden", + "id": "status.media_hidden" + } + ], + "path": "app/javascript/mastodon/components/video_player.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Delete", + "id": "confirmations.delete.confirm" + }, + { + "defaultMessage": "Are you sure you want to delete this status?", + "id": "confirmations.delete.message" + }, + { + "defaultMessage": "Block", + "id": "confirmations.block.confirm" + }, + { + "defaultMessage": "Mute", + "id": "confirmations.mute.confirm" + }, + { + "defaultMessage": "Are you sure you want to block {name}?", + "id": "confirmations.block.message" + }, + { + "defaultMessage": "Are you sure you want to mute {name}?", + "id": "confirmations.mute.message" + } + ], + "path": "app/javascript/mastodon/containers/status_container.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Block", + "id": "confirmations.block.confirm" + }, + { + "defaultMessage": "Mute", + "id": "confirmations.mute.confirm" + }, + { + "defaultMessage": "Are you sure you want to block {name}?", + "id": "confirmations.block.message" + }, + { + "defaultMessage": "Are you sure you want to mute {name}?", + "id": "confirmations.mute.message" + } + ], + "path": "app/javascript/mastodon/features/account_timeline/containers/header_container.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Mention @{name}", + "id": "account.mention" + }, + { + "defaultMessage": "Edit profile", + "id": "account.edit_profile" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" + }, + { + "defaultMessage": "Unfollow", + "id": "account.unfollow" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + }, + { + "defaultMessage": "Block @{name}", + "id": "account.block" + }, + { + "defaultMessage": "Mute @{name}", + "id": "account.mute" + }, + { + "defaultMessage": "Follow", + "id": "account.follow" + }, + { + "defaultMessage": "Report @{name}", + "id": "account.report" + }, + { + "defaultMessage": "This user is from another instance. This number may be larger.", + "id": "account.disclaimer" + }, + { + "defaultMessage": "Posts", + "id": "account.posts" + }, + { + "defaultMessage": "Follows", + "id": "account.follows" + }, + { + "defaultMessage": "Followers", + "id": "account.followers" + } + ], + "path": "app/javascript/mastodon/features/account/components/action_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Unfollow", + "id": "account.unfollow" + }, + { + "defaultMessage": "Follow", + "id": "account.follow" + }, + { + "defaultMessage": "Awaiting approval", + "id": "account.requested" + }, + { + "defaultMessage": "Follows you", + "id": "account.follows_you" + } + ], + "path": "app/javascript/mastodon/features/account/components/header.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Blocked users", + "id": "column.blocks" + } + ], + "path": "app/javascript/mastodon/features/blocks/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Local timeline", + "id": "column.community" + }, + { + "defaultMessage": "The local timeline is empty. Write something publicly to get the ball rolling!", + "id": "empty_column.community" + } + ], + "path": "app/javascript/mastodon/features/community_timeline/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "What is on your mind?", + "id": "compose_form.placeholder" + }, + { + "defaultMessage": "Content warning", + "id": "compose_form.spoiler_placeholder" + }, + { + "defaultMessage": "Toot", + "id": "compose_form.publish" + } + ], + "path": "app/javascript/mastodon/features/compose/components/compose_form.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Insert emoji", + "id": "emoji_button.label" + }, + { + "defaultMessage": "Search...", + "id": "emoji_button.search" + }, + { + "defaultMessage": "People", + "id": "emoji_button.people" + }, + { + "defaultMessage": "Nature", + "id": "emoji_button.nature" + }, + { + "defaultMessage": "Food & Drink", + "id": "emoji_button.food" + }, + { + "defaultMessage": "Activity", + "id": "emoji_button.activity" + }, + { + "defaultMessage": "Travel & Places", + "id": "emoji_button.travel" + }, + { + "defaultMessage": "Objects", + "id": "emoji_button.objects" + }, + { + "defaultMessage": "Symbols", + "id": "emoji_button.symbols" + }, + { + "defaultMessage": "Flags", + "id": "emoji_button.flags" + } + ], + "path": "app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Edit profile", + "id": "navigation_bar.edit_profile" + } + ], + "path": "app/javascript/mastodon/features/compose/components/navigation_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Public", + "id": "privacy.public.short" + }, + { + "defaultMessage": "Post to public timelines", + "id": "privacy.public.long" + }, + { + "defaultMessage": "Unlisted", + "id": "privacy.unlisted.short" + }, + { + "defaultMessage": "Do not show in public timelines", + "id": "privacy.unlisted.long" + }, + { + "defaultMessage": "Followers-only", + "id": "privacy.private.short" + }, + { + "defaultMessage": "Post to followers only", + "id": "privacy.private.long" + }, + { + "defaultMessage": "Direct", + "id": "privacy.direct.short" + }, + { + "defaultMessage": "Post to mentioned users only", + "id": "privacy.direct.long" + }, + { + "defaultMessage": "Adjust status privacy", + "id": "privacy.change" + } + ], + "path": "app/javascript/mastodon/features/compose/components/privacy_dropdown.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Cancel", + "id": "reply_indicator.cancel" + } + ], + "path": "app/javascript/mastodon/features/compose/components/reply_indicator.json" + }, + { + "descriptors": [ + { + "defaultMessage": "{count, number} {count, plural, one {result} other {results}}", + "id": "search_results.total" + } + ], + "path": "app/javascript/mastodon/features/compose/components/search_results.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Search", + "id": "search.placeholder" + } + ], + "path": "app/javascript/mastodon/features/compose/components/search.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Add media", + "id": "upload_button.label" + } + ], + "path": "app/javascript/mastodon/features/compose/components/upload_button.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Undo", + "id": "upload_form.undo" + } + ], + "path": "app/javascript/mastodon/features/compose/components/upload_form.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Uploading...", + "id": "upload_progress.label" + } + ], + "path": "app/javascript/mastodon/features/compose/components/upload_progress.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Mark media as sensitive", + "id": "compose_form.sensitive" + } + ], + "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Hide text behind warning", + "id": "compose_form.spoiler" + } + ], + "path": "app/javascript/mastodon/features/compose/containers/spoiler_button_container.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "id": "compose_form.lock_disclaimer" + }, + { + "defaultMessage": "locked", + "id": "compose_form.lock_disclaimer.lock" + }, + { + "defaultMessage": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", + "id": "compose_form.privacy_disclaimer" + } + ], + "path": "app/javascript/mastodon/features/compose/containers/warning_container.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Getting started", + "id": "getting_started.heading" + }, + { + "defaultMessage": "Federated timeline", + "id": "navigation_bar.public_timeline" + }, + { + "defaultMessage": "Local timeline", + "id": "navigation_bar.community_timeline" + }, + { + "defaultMessage": "Preferences", + "id": "navigation_bar.preferences" + }, + { + "defaultMessage": "Logout", + "id": "navigation_bar.logout" + } + ], + "path": "app/javascript/mastodon/features/compose/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Favourites", + "id": "column.favourites" + } + ], + "path": "app/javascript/mastodon/features/favourited_statuses/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Authorize", + "id": "follow_request.authorize" + }, + { + "defaultMessage": "Reject", + "id": "follow_request.reject" + } + ], + "path": "app/javascript/mastodon/features/follow_requests/components/account_authorize.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Follow requests", + "id": "column.follow_requests" + } + ], + "path": "app/javascript/mastodon/features/follow_requests/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Getting started", + "id": "getting_started.heading" + }, + { + "defaultMessage": "Federated timeline", + "id": "navigation_bar.public_timeline" + }, + { + "defaultMessage": "Navigation", + "id": "column_subheading.navigation" + }, + { + "defaultMessage": "Settings", + "id": "column_subheading.settings" + }, + { + "defaultMessage": "Local timeline", + "id": "navigation_bar.community_timeline" + }, + { + "defaultMessage": "Preferences", + "id": "navigation_bar.preferences" + }, + { + "defaultMessage": "Follow requests", + "id": "navigation_bar.follow_requests" + }, + { + "defaultMessage": "Logout", + "id": "navigation_bar.logout" + }, + { + "defaultMessage": "Favourites", + "id": "navigation_bar.favourites" + }, + { + "defaultMessage": "Blocked users", + "id": "navigation_bar.blocks" + }, + { + "defaultMessage": "Muted users", + "id": "navigation_bar.mutes" + }, + { + "defaultMessage": "Extended information", + "id": "navigation_bar.info" + }, + { + "defaultMessage": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", + "id": "getting_started.open_source_notice" + }, + { + "defaultMessage": "Various apps are available", + "id": "getting_started.apps" + } + ], + "path": "app/javascript/mastodon/features/getting_started/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "There is nothing in this hashtag yet.", + "id": "empty_column.hashtag" + } + ], + "path": "app/javascript/mastodon/features/hashtag_timeline/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Filter out by regular expressions", + "id": "home.column_settings.filter_regex" + }, + { + "defaultMessage": "Column settings", + "id": "home.settings" + }, + { + "defaultMessage": "Basic", + "id": "home.column_settings.basic" + }, + { + "defaultMessage": "Show boosts", + "id": "home.column_settings.show_reblogs" + }, + { + "defaultMessage": "Show replies", + "id": "home.column_settings.show_replies" + }, + { + "defaultMessage": "Advanced", + "id": "home.column_settings.advanced" + } + ], + "path": "app/javascript/mastodon/features/home_timeline/components/column_settings.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Home", + "id": "column.home" + }, + { + "defaultMessage": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "id": "empty_column.home" + }, + { + "defaultMessage": "the public timeline", + "id": "empty_column.home.public_timeline" + } + ], + "path": "app/javascript/mastodon/features/home_timeline/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Muted users", + "id": "column.mutes" + } + ], + "path": "app/javascript/mastodon/features/mutes/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Clear notifications", + "id": "notifications.clear" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/clear_column_button.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Column settings", + "id": "notifications.settings" + }, + { + "defaultMessage": "Desktop notifications", + "id": "notifications.column_settings.alert" + }, + { + "defaultMessage": "Show in column", + "id": "notifications.column_settings.show" + }, + { + "defaultMessage": "Play sound", + "id": "notifications.column_settings.sound" + }, + { + "defaultMessage": "New followers:", + "id": "notifications.column_settings.follow" + }, + { + "defaultMessage": "Favourites:", + "id": "notifications.column_settings.favourite" + }, + { + "defaultMessage": "Mentions:", + "id": "notifications.column_settings.mention" + }, + { + "defaultMessage": "Boosts:", + "id": "notifications.column_settings.reblog" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/column_settings.json" + }, + { + "descriptors": [ + { + "defaultMessage": "{name} followed you", + "id": "notification.follow" + }, + { + "defaultMessage": "{name} favourited your status", + "id": "notification.favourite" + }, + { + "defaultMessage": "{name} boosted your status", + "id": "notification.reblog" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/notification.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Notifications", + "id": "column.notifications" + }, + { + "defaultMessage": "Are you sure you want to permanently clear all your notifications?", + "id": "notifications.clear_confirmation" + }, + { + "defaultMessage": "Clear notifications", + "id": "notifications.clear" + }, + { + "defaultMessage": "You don't have any notifications yet. Interact with others to start the conversation.", + "id": "empty_column.notifications" + } + ], + "path": "app/javascript/mastodon/features/notifications/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Federated timeline", + "id": "column.public" + }, + { + "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "id": "empty_column.public" + } + ], + "path": "app/javascript/mastodon/features/public_timeline/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "New report", + "id": "report.heading" + }, + { + "defaultMessage": "Additional comments", + "id": "report.placeholder" + }, + { + "defaultMessage": "Submit", + "id": "report.submit" + }, + { + "defaultMessage": "Reporting", + "id": "report.target" + } + ], + "path": "app/javascript/mastodon/features/report/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Delete", + "id": "status.delete" + }, + { + "defaultMessage": "Mention @{name}", + "id": "status.mention" + }, + { + "defaultMessage": "Reply", + "id": "status.reply" + }, + { + "defaultMessage": "Boost", + "id": "status.reblog" + }, + { + "defaultMessage": "This post cannot be boosted", + "id": "status.cannot_reblog" + }, + { + "defaultMessage": "Favourite", + "id": "status.favourite" + }, + { + "defaultMessage": "Report @{name}", + "id": "status.report" + } + ], + "path": "app/javascript/mastodon/features/status/components/action_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Delete", + "id": "confirmations.delete.confirm" + }, + { + "defaultMessage": "Are you sure you want to delete this status?", + "id": "confirmations.delete.message" + } + ], + "path": "app/javascript/mastodon/features/status/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Boost", + "id": "status.reblog" + }, + { + "defaultMessage": "You can press {combo} to skip this next time", + "id": "boost_modal.combo" + } + ], + "path": "app/javascript/mastodon/features/ui/components/boost_modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Cancel", + "id": "confirmation_modal.cancel" + } + ], + "path": "app/javascript/mastodon/features/ui/components/confirmation_modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Close", + "id": "lightbox.close" + } + ], + "path": "app/javascript/mastodon/features/ui/components/media_modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Home", + "id": "column.home" + }, + { + "defaultMessage": "Notifications", + "id": "column.notifications" + }, + { + "defaultMessage": "Local timeline", + "id": "column.community" + }, + { + "defaultMessage": "Federated timeline", + "id": "column.public" + }, + { + "defaultMessage": "Welcome to Mastodon!", + "id": "onboarding.page_one.welcome" + }, + { + "defaultMessage": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "id": "onboarding.page_one.federation" + }, + { + "defaultMessage": "You are on {domain}, so your full handle is {handle}", + "id": "onboarding.page_one.handle" + }, + { + "defaultMessage": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "id": "onboarding.page_two.compose" + }, + { + "defaultMessage": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "id": "onboarding.page_three.search" + }, + { + "defaultMessage": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "id": "onboarding.page_three.profile" + }, + { + "defaultMessage": "The home timeline shows posts from people you follow.", + "id": "onboarding.page_four.home" + }, + { + "defaultMessage": "The notifications column shows when someone interacts with you.", + "id": "onboarding.page_four.notifications" + }, + { + "defaultMessage": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "id": "onboarding.page_five.public_timelines" + }, + { + "defaultMessage": "Your instance's admin is {admin}.", + "id": "onboarding.page_six.admin" + }, + { + "defaultMessage": "Please read {domain}'s {guidelines}!", + "id": "onboarding.page_six.read_guidelines" + }, + { + "defaultMessage": "community guidelines", + "id": "onboarding.page_six.guidelines" + }, + { + "defaultMessage": "Almost done...", + "id": "onboarding.page_six.almost_done" + }, + { + "defaultMessage": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "id": "onboarding.page_six.github" + }, + { + "defaultMessage": "There are {apps} available for iOS, Android and other platforms.", + "id": "onboarding.page_six.apps_available" + }, + { + "defaultMessage": "mobile apps", + "id": "onboarding.page_six.various_app" + }, + { + "defaultMessage": "Bon Appetoot!", + "id": "onboarding.page_six.appetoot" + }, + { + "defaultMessage": "Next", + "id": "onboarding.next" + }, + { + "defaultMessage": "Done", + "id": "onboarding.done" + }, + { + "defaultMessage": "Skip", + "id": "onboarding.skip" + } + ], + "path": "app/javascript/mastodon/features/ui/components/onboarding_modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Compose", + "id": "tabs_bar.compose" + }, + { + "defaultMessage": "Home", + "id": "tabs_bar.home" + }, + { + "defaultMessage": "Notifications", + "id": "tabs_bar.notifications" + }, + { + "defaultMessage": "Local", + "id": "tabs_bar.local_timeline" + }, + { + "defaultMessage": "Federated", + "id": "tabs_bar.federated_timeline" + } + ], + "path": "app/javascript/mastodon/features/ui/components/tabs_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Drag & drop to upload", + "id": "upload_area.title" + } + ], + "path": "app/javascript/mastodon/features/ui/components/upload_area.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Close", + "id": "lightbox.close" + } + ], + "path": "app/javascript/mastodon/features/ui/components/video_modal.json" + } +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json @@ -0,0 +1,163 @@ +{ + "account.block": "Block @{name}", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Edit profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.follows": "Follows", + "account.follows_you": "Follows you", + "account.mention": "Mention @{name}", + "account.mute": "Mute @{name}", + "account.posts": "Posts", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.unblock": "Unblock @{name}", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.privacy_disclaimer": "Your post will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is not a public post, and it may be boosted or otherwise made visible to unintended recipients.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Mark media as sensitive", + "compose_form.spoiler": "Hide text behind warning", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Getting started", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Close", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not post to public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Cancel", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Search", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Delete", + "status.favourite": "Favourite", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.open": "Expand this status", + "status.reblog": "Boost", + "status.reblogged_by": "{name} boosted", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Click to view", + "status.sensitive_warning": "Sensitive content", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Compose", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media", + "upload_form.undo": "Undo", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Toggle sound", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json @@ -0,0 +1,163 @@ +{ + "account.block": "Bloki @{name}", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Redakti la profilon", + "account.follow": "Sekvi", + "account.followers": "Sekvantoj", + "account.follows": "Sekvatoj", + "account.follows_you": "Sekvas vin", + "account.mention": "Mencii @{name}", + "account.mute": "Mute @{name}", + "account.posts": "Mesaĝoj", + "account.report": "Report @{name}", + "account.requested": "Atendas aprobon", + "account.unblock": "Malbloki @{name}", + "account.unfollow": "Malsekvi", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Loka tempolinio", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Hejmo", + "column.mutes": "Muted users", + "column.notifications": "Sciigoj", + "column.public": "Fratara tempolinio", + "column_back_button.label": "Reveni", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Pri kio vi pensas?", + "compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.", + "compose_form.publish": "Hup", + "compose_form.sensitive": "Marki ke la enhavo estas tikla", + "compose_form.spoiler": "Kaŝi la tekston malantaŭ averto", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Por komenci", + "getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Fermi", + "loading_indicator.label": "Ŝarĝanta...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Loka tempolinio", + "navigation_bar.edit_profile": "Redakti la profilon", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Elsaluti", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Preferoj", + "navigation_bar.public_timeline": "Fratara tempolinio", + "notification.favourite": "{name} favoris vian mesaĝon", + "notification.follow": "{name} sekvis vin", + "notification.reblog": "{name} diskonigis vian mesaĝon", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Retumilaj atentigoj", + "notifications.column_settings.favourite": "Favoroj:", + "notifications.column_settings.follow": "Novaj sekvantoj:", + "notifications.column_settings.mention": "Mencioj:", + "notifications.column_settings.reblog": "Diskonigoj:", + "notifications.column_settings.show": "Montri en kolono", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Rezigni", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Serĉi", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Forigi", + "status.favourite": "Favori", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mencii @{name}", + "status.open": "Expand this status", + "status.reblog": "Diskonigi", + "status.reblogged_by": "{name} diskonigita", + "status.reply": "Respondi", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Alklaki por vidi", + "status.sensitive_warning": "Tikla enhavo", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Ekskribi", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Hejmo", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Sciigoj", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Aldoni enhavaĵon", + "upload_form.undo": "Malfari", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Aktivigi sonojn", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json @@ -0,0 +1,163 @@ +{ + "account.block": "Bloquear", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Editar perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.follows": "Seguir", + "account.follows_you": "Te sigue", + "account.mention": "Mencionar", + "account.mute": "Silenciar", + "account.posts": "Publicaciones", + "account.report": "Report @{name}", + "account.requested": "Esperando aprobación", + "account.unblock": "Desbloquear", + "account.unfollow": "Dejar de seguir", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Usuarios bloqueados", + "column.community": "Historia local", + "column.favourites": "Favoritos", + "column.follow_requests": "Solicitudes para seguirte", + "column.home": "Inicio", + "column.mutes": "Usuarios silenciados", + "column.notifications": "Notificaciones", + "column.public": "Historia federada", + "column_back_button.label": "Atrás", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "¿En qué estás pensando?", + "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", + "compose_form.publish": "Tootear", + "compose_form.sensitive": "Marcar contenido como sensible", + "compose_form.spoiler": "Ocultar texto tras advertencia", + "compose_form.spoiler_placeholder": "Advertencia de contenido", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insertar emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Primeros pasos", + "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Cerrar", + "loading_indicator.label": "Cargando...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Usuarios bloqueados", + "navigation_bar.community_timeline": "Historia local", + "navigation_bar.edit_profile": "Editar perfil", + "navigation_bar.favourites": "Favoritos", + "navigation_bar.follow_requests": "Solicitudes para seguirte", + "navigation_bar.info": "Información adicional", + "navigation_bar.logout": "Cerrar sesión", + "navigation_bar.mutes": "Usuarios silenciados", + "navigation_bar.preferences": "Preferencias", + "navigation_bar.public_timeline": "Historia federada", + "notification.favourite": "{name} marcó tu estado como favorito", + "notification.follow": "{name} te empezó a seguir", + "notification.reblog": "{name} ha retooteado tu estado", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Notificaciones de escritorio", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.follow": "Nuevos seguidores:", + "notifications.column_settings.mention": "Menciones:", + "notifications.column_settings.reblog": "Retoots:", + "notifications.column_settings.show": "Mostrar en columna", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Ajustar privacidad", + "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Sólo mostrar a seguidores", + "privacy.private.short": "Privado", + "privacy.public.long": "Mostrar en la historia federada", + "privacy.public.short": "Público", + "privacy.unlisted.long": "No mostrar en la historia federada", + "privacy.unlisted.short": "Sin federar", + "reply_indicator.cancel": "Cancelar", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Buscar", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Borrar", + "status.favourite": "Favorito", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mencionar", + "status.open": "Expandir estado", + "status.reblog": "Retoot", + "status.reblogged_by": "Retooteado por {name}", + "status.reply": "Responder", + "status.replyAll": "Reply to thread", + "status.report": "Reportar", + "status.sensitive_toggle": "Click para ver", + "status.sensitive_warning": "Contenido sensible", + "status.show_less": "Mostrar menos", + "status.show_more": "Mostrar más", + "tabs_bar.compose": "Redactar", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Inicio", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificaciones", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Subir multimedia", + "upload_form.undo": "Deshacer", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Act/Desac. sonido", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json @@ -0,0 +1,163 @@ +{ + "account.block": "@{name} را مسدود کن", + "account.disclaimer": "این کاربر عضو سرور متفاوتی است. شاید عدد واقعی بیشتر از این باشد.", + "account.edit_profile": "ویرایش نمایه", + "account.follow": "پی بگیرید", + "account.followers": "پیگیران", + "account.follows": "پی می‌گیرد", + "account.follows_you": "پیگیر شماست", + "account.mention": "نام‌بردن از @{name}", + "account.mute": "بی‌صدا کردن @{name}", + "account.posts": "نوشته‌ها", + "account.report": "گزارش @{name}", + "account.requested": "در انتظار پذیرش", + "account.unblock": "رفع انسداد @{name}", + "account.unfollow": "پایان پیگیری", + "account.unmute": "باصدا کردن @{name}", + "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", + "column.blocks": "کاربران مسدودشده", + "column.community": "نوشته‌های محلی", + "column.favourites": "پسندیده‌ها", + "column.follow_requests": "درخواست‌های پیگیری", + "column.home": "خانه", + "column.mutes": "کاربران بی‌صداشده", + "column.notifications": "اعلان‌ها", + "column.public": "نوشته‌های همه‌جا", + "column_back_button.label": "بازگشت", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "تازه چه خبر؟", + "compose_form.privacy_disclaimer": "نوشتهٔ خصوصی شما به کاربران نام‌برده‌شده در {domains} فرستاده می‌شود. آیا به {domainsCount, plural, one {آن سرور} other {آن سرورها}} اعتماد دارید؟ تنظیمات حریم خصوصی نوشته‌ها تنها در سرورهای ماستدون کار می‌کند. اگر {domains} {domainsCount, plural, one {یک سرور ماستدون نباشد} other {سرورهای ماستدون نباشند}}، اشاره‌ای به خصوصی‌بودن نوشتهٔ شما نخواهد شد و شاید نوشتهٔ شما هم‌رسان شود یا برای کاربرانی که نمی‌خواهید نمایش یابد.", + "compose_form.publish": "بوق", + "compose_form.sensitive": "تصاویر حساس هستند", + "compose_form.spoiler": "نوشته را پشت هشدار پنهان کنید", + "compose_form.spoiler_placeholder": "هشدار محتوا", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "فعالیت", + "emoji_button.flags": "پرچم‌ها", + "emoji_button.food": "غذا و نوشیدنی", + "emoji_button.label": "افزودن شکلک", + "emoji_button.nature": "طبیعت", + "emoji_button.objects": "اشیا", + "emoji_button.people": "مردم", + "emoji_button.search": "جستجو...", + "emoji_button.symbols": "نمادها", + "emoji_button.travel": "سفر و مکان", + "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!", + "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.", + "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.", + "empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا", + "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.", + "empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود", + "follow_request.authorize": "اجازه دهید", + "follow_request.reject": "اجازه ندهید", + "getting_started.apps": "اپ‌های گوناگونی در دسترس‌اند", + "getting_started.heading": "آغاز کنید", + "getting_started.open_source_notice": "ماستدون یک نرم‌افزار آزاد است. می‌توانید در ساخت آن مشارکت کنید یا مشکلاتش را در {github} گزارش دهید. {apps}.", + "home.column_settings.advanced": "پیشرفته", + "home.column_settings.basic": "اصلی", + "home.column_settings.filter_regex": "با عبارت‌های باقاعده فیلتر کنید", + "home.column_settings.show_reblogs": "نمایش بازبوق‌ها", + "home.column_settings.show_replies": "نمایش پاسخ‌ها", + "home.settings": "تنظیمات ستون", + "lightbox.close": "بستن", + "loading_indicator.label": "بارگیری...", + "media_gallery.toggle_visible": "تغییر پیدایی", + "missing_indicator.label": "پیدا نشد", + "navigation_bar.blocks": "کاربران مسدودشده", + "navigation_bar.community_timeline": "نوشته‌های محلی", + "navigation_bar.edit_profile": "ویرایش نمایه", + "navigation_bar.favourites": "پسندیده‌ها", + "navigation_bar.follow_requests": "درخواست‌های پیگیری", + "navigation_bar.info": "اطلاعات تکمیلی", + "navigation_bar.logout": "خروج", + "navigation_bar.mutes": "کاربران بی‌صداشده", + "navigation_bar.preferences": "ترجیحات", + "navigation_bar.public_timeline": "نوشته‌های همه‌جا", + "notification.favourite": "{name} نوشتهٔ شما را پسندید", + "notification.follow": "{name} پیگیر شما شد", + "notification.reblog": "{name} نوشتهٔ شما را بازبوقید", + "notifications.clear": "پاک‌کردن اعلان‌ها", + "notifications.clear_confirmation": "واقعاً می‌خواهید همهٔ اعلان‌هایتان را برای همیشه پاک کنید؟", + "notifications.column_settings.alert": "اعلان در کامپیوتر", + "notifications.column_settings.favourite": "پسندیده‌ها:", + "notifications.column_settings.follow": "پیگیران تازه:", + "notifications.column_settings.mention": "نام‌بردن‌ها:", + "notifications.column_settings.reblog": "بازبوق‌ها:", + "notifications.column_settings.show": "در ستون نشان بده", + "notifications.column_settings.sound": "صدا را پخش کن", + "notifications.settings": "تنظیمات ستون", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "تنظیم حریم خصوصی نوشته‌ها", + "privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده", + "privacy.direct.short": "مستقیم", + "privacy.private.long": "تنها به پیگیران نشان بده", + "privacy.private.short": "خصوصی", + "privacy.public.long": "در فهرست نوشته‌های عمومی نشان بده", + "privacy.public.short": "عمومی", + "privacy.unlisted.long": "در فهرست نوشته‌های همه‌جا نشان نده", + "privacy.unlisted.short": "فهرست‌نشده", + "reply_indicator.cancel": "لغو", + "report.heading": "گزارش تازه", + "report.placeholder": "توضیح اضافه", + "report.submit": "بفرست", + "report.target": "گزارش‌دادن", + "search.placeholder": "جستجو", + "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", + "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", + "status.delete": "پاک‌کردن", + "status.favourite": "پسندیدن", + "status.load_more": "بیشتر نشان بده", + "status.media_hidden": "تصویر پنهان شده", + "status.mention": "از @{name} نام ببرید", + "status.open": "این نوشته را باز کن", + "status.reblog": "بوق", + "status.reblogged_by": "{name} بازبوقید", + "status.reply": "پاسخ", + "status.replyAll": "به نوشته پاسخ دهید", + "status.report": "@{name} را گزارش دهید", + "status.sensitive_toggle": "برای دیدن کلیک کنید", + "status.sensitive_warning": "محتوای حساس", + "status.show_less": "نهفتن", + "status.show_more": "نمایش", + "tabs_bar.compose": "بنویسید", + "tabs_bar.federated_timeline": "همگانی", + "tabs_bar.home": "خانه", + "tabs_bar.local_timeline": "محلی", + "tabs_bar.notifications": "اعلان‌ها", + "upload_area.title": "برای بارگذاری به این‌جا بکشید", + "upload_button.label": "افزودن تصویر", + "upload_form.undo": "واگردانی", + "upload_progress.label": "بارگذاری...", + "video_player.expand": "بازکردن ویدیو", + "video_player.toggle_sound": "تغییر صداداری", + "video_player.toggle_visible": "تغییر پیدایی", + "video_player.video_error": "ویدیو نمی‌تواند پخش شود" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json @@ -0,0 +1,163 @@ +{ + "account.block": "Estä @{name}", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Muokkaa", + "account.follow": "Seuraa", + "account.followers": "Seuraajia", + "account.follows": "Seuraa", + "account.follows_you": "Seuraa sinua", + "account.mention": "Mainitse @{name}", + "account.mute": "Mute @{name}", + "account.posts": "Postit", + "account.report": "Report @{name}", + "account.requested": "Odottaa hyväksyntää", + "account.unblock": "Salli @{name}", + "account.unfollow": "Lopeta seuraaminen", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Paikallinen aikajana", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Koti", + "column.mutes": "Muted users", + "column.notifications": "Ilmoitukset", + "column.public": "Yleinen aikajana", + "column_back_button.label": "Takaisin", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Mitä sinulla on mielessä?", + "compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Merkitse media herkäksi", + "compose_form.spoiler": "Piiloita teksti varoituksen taakse", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Aloitus", + "getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Sulje", + "loading_indicator.label": "Ladataan...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Paikallinen aikajana", + "navigation_bar.edit_profile": "Muokkaa profiilia", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Kirjaudu ulos", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Ominaisuudet", + "navigation_bar.public_timeline": "Yleinen aikajana", + "notification.favourite": "{name} tykkäsi statuksestasi", + "notification.follow": "{name} seurasi sinua", + "notification.reblog": "{name} buustasi statustasi", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Työpöytä ilmoitukset", + "notifications.column_settings.favourite": "Tykkäyksiä:", + "notifications.column_settings.follow": "Uusia seuraajia:", + "notifications.column_settings.mention": "Mainintoja:", + "notifications.column_settings.reblog": "Buusteja:", + "notifications.column_settings.show": "Näytä sarakkeessa", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Peruuta", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Hae", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Poista", + "status.favourite": "Tykkää", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mainitse @{name}", + "status.open": "Expand this status", + "status.reblog": "Buustaa", + "status.reblogged_by": "{name} buustasi", + "status.reply": "Vastaa", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Klikkaa nähdäksesi", + "status.sensitive_warning": "Arkaluontoista sisältöä", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Luo", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Koti", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Ilmoitukset", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Lisää mediaa", + "upload_form.undo": "Peru", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Äänet päälle/pois", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json @@ -0,0 +1,163 @@ +{ + "account.block": "Bloquer", + "account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.", + "account.edit_profile": "Modifier le profil", + "account.follow": "Suivre", + "account.followers": "Abonné⋅e⋅s", + "account.follows": "Abonnements", + "account.follows_you": "Vous suit", + "account.mention": "Mentionner", + "account.mute": "Masquer", + "account.posts": "Statuts", + "account.report": "Signaler", + "account.requested": "Invitation envoyée", + "account.unblock": "Débloquer", + "account.unfollow": "Ne plus suivre", + "account.unmute": "Ne plus masquer", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Comptes bloqués", + "column.community": "Fil public local", + "column.favourites": "Favoris", + "column.follow_requests": "Demandes de suivi", + "column.home": "Accueil", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.public": "Fil public global", + "column_back_button.label": "Retour", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Qu’avez-vous en tête ?", + "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", + "compose_form.private": "Rendre privé", + "compose_form.publish": "Pouet", + "compose_form.sensitive": "Marquer le média comme délicat", + "compose_form.spoiler": "Masquer le texte derrière un avertissement", + "compose_form.spoiler_placeholder": "Avertissement", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insérer un emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", + "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag", + "empty_column.home.public_timeline": "le fil public", + "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.", + "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", + "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.", + "follow_request.authorize": "Autoriser", + "follow_request.reject": "Rejeter", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Pour commencer", + "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", + "home.column_settings.advanced": "Avancé", + "home.column_settings.basic": "Basique", + "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", + "home.column_settings.show_reblogs": "Afficher les partages", + "home.column_settings.show_replies": "Afficher les réponses", + "home.settings": "Paramètres de la colonne", + "lightbox.close": "Fermer", + "loading_indicator.label": "Chargement…", + "media_gallery.toggle_visible": "Modifier la visibilité", + "missing_indicator.label": "Non trouvé", + "navigation_bar.blocks": "Comptes bloqués", + "navigation_bar.community_timeline": "Fil public local", + "navigation_bar.edit_profile": "Modifier le profil", + "navigation_bar.favourites": "Favoris", + "navigation_bar.follow_requests": "Demandes de suivi", + "navigation_bar.info": "Plus d’informations", + "navigation_bar.logout": "Déconnexion", + "navigation_bar.mutes": "Comptes silencés", + "navigation_bar.preferences": "Préférences", + "navigation_bar.public_timeline": "Fil public global", + "notification.favourite": "{name} a ajouté à ses favoris :", + "notification.follow": "{name} vous suit.", + "notification.reblog": "{name} a partagé votre statut :", + "notifications.clear": "Nettoyer", + "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", + "notifications.column_settings.alert": "Notifications locales", + "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.follow": "Nouveaux abonné⋅e⋅s :", + "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.reblog": "Partages :", + "notifications.column_settings.show": "Afficher dans la colonne", + "notifications.column_settings.sound": "Émettre un son", + "notifications.settings": "Paramètres de la colonne", + "onboarding.done": "Done", + "onboarding.next": "Suivant", + "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.", + "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez", + "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous", + "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", + "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}", + "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", + "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}", + "onboarding.page_six.almost_done": "Nous y sommes presque…", + "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!", + "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", + "onboarding.page_six.guidelines": "règles de la communauté", + "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", + "onboarding.page_six.various_app": "applications mobiles", + "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.", + "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.", + "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", + "onboarding.skip": "Passer", + "privacy.change": "Ajuster la confidentialité du message", + "privacy.direct.long": "N’afficher que pour les personnes mentionnées", + "privacy.direct.short": "Direct", + "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", + "privacy.private.short": "Privé", + "privacy.public.long": "Afficher dans les fils publics", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Ne pas afficher dans les fils publics", + "privacy.unlisted.short": "Non-listé", + "reply_indicator.cancel": "Annuler", + "report.heading": "Nouveau signalement", + "report.placeholder": "Commentaires additionnels", + "report.submit": "Envoyer", + "report.target": "Signalement", + "search.placeholder": "Rechercher", + "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Effacer", + "status.favourite": "Ajouter aux favoris", + "status.load_more": "Charger plus", + "status.media_hidden": "Média caché", + "status.mention": "Mentionner", + "status.open": "Déplier ce statut", + "status.reblog": "Partager", + "status.reblogged_by": "{name} a partagé :", + "status.reply": "Répondre", + "status.replyAll": "Reply to thread", + "status.report": "Signaler @{name}", + "status.sensitive_toggle": "Cliquer pour dévoiler", + "status.sensitive_warning": "Contenu délicat", + "status.show_less": "Replier", + "status.show_more": "Déplier", + "tabs_bar.compose": "Composer", + "tabs_bar.federated_timeline": "Fil public global", + "tabs_bar.home": "Accueil", + "tabs_bar.local_timeline": "Fil public local", + "tabs_bar.notifications": "Notifications", + "upload_area.title": "Glissez et déposez pour envoyer", + "upload_button.label": "Joindre un média", + "upload_form.undo": "Annuler", + "upload_progress.label": "Envoi en cours…", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Mettre/Couper le son", + "video_player.toggle_visible": "Afficher/Cacher la vidéo", + "video_player.video_error": "Video could not be played" +} diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json @@ -0,0 +1,165 @@ +{ + "account.block": "חסימת @{name}", + "account.disclaimer": "משתמש זה מגיע מקהילה אחרת. המספר הזה עשוי להיות גדול יותר.", + "account.edit_profile": "עריכת פרופיל", + "account.follow": "מעקב", + "account.followers": "עוקבים", + "account.follows_you": "במעקב אחריך", + "account.follows": "נעקבים", + "account.mention": "אזכור של @{name}", + "account.mute": "להשתיק את @{name}", + "account.posts": "הודעות", + "account.report": "לדווח על @{name}", + "account.requested": "בהמתנה לאישור", + "account.unblock": "הסרת חסימה מעל @{name}", + "account.unfollow": "הפסקת מעקב", + "account.unmute": "הפסקת השתקת @{name}", + "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה", + "column.blocks": "חסימות", + "column.community": "פיד מקומי", + "column.favourites": "חיבובים", + "column.follow_requests": "בקשות מעקב", + "column.home": "בבית", + "column.mutes": "השתקות", + "column.notifications": "התראות", + "column.public": "בפרהסיה", + "column_back_button.label": "אחורה", + "column_subheading.navigation": "ניווט", + "column_subheading.settings": "אפשרויות", + "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.", + "compose_form.lock_disclaimer.lock": "נעול", + "compose_form.placeholder": "מה עובר לך בראש?", + "compose_form.privacy_disclaimer": "הודעתך הפרטית תשלח למשתמשים על {domains}. האם ניתן לסמוך על {domainsCount, plural, one {שרת זה} other {שרתים אלו}}? פרטיות ההודעה קיימת רק על שרתי מסטודון. אם {domains} {domainsCount, plural, one {הוא לא שרת מסטודון} other {הם לא שרתי מסטודון}}, לא יהיה שום סימן שההודעה פרטית, והוא עשוי להיות מקודם או להחשף למשתמשים שלא ברשימת היעד.", + "compose_form.publish": "לחצרץ", + "compose_form.sensitive": "סימון תוכן כרגיש", + "compose_form.spoiler": "הסתרה מאחורי אזהרת תוכן", + "compose_form.spoiler_placeholder": "אזהרת תוכן", + "confirmation_modal.cancel": "ביטול", + "confirmations.block.confirm": "לחסום", + "confirmations.block.message": "לחסום את {name}?", + "confirmations.delete.confirm": "למחוק", + "confirmations.delete.message": "למחוק את ההודעה?", + "confirmations.mute.confirm": "להשתיק", + "confirmations.mute.message": "להשתיק את {name}?", + "emoji_button.activity": "פעילות", + "emoji_button.flags": "דגלים", + "emoji_button.food": "אוכל ושתיה", + "emoji_button.label": "הוספת אמוג'י", + "emoji_button.nature": "טבע", + "emoji_button.objects": "חפצים", + "emoji_button.people": "אנשים", + "emoji_button.search": "חיפוש...", + "emoji_button.symbols": "סמלים", + "emoji_button.travel": "טיולים ואתרים", + "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!", + "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.", + "empty_column.home.public_timeline": "בפרהסיה", + "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.", + "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!", + "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.", + "follow_request.authorize": "קבלה", + "follow_request.reject": "דחיה", + "getting_started.apps": "קיים מבחר יישומונים לניידים", + "getting_started.heading": "על ההתחלה", + "getting_started.open_source_notice": "מסטודון היא תוכנה חופשית (בקוד פתוח). ניתן לתרום או לדווח על בעיות בגיטהאב: {github}. {apps}.", + "home.column_settings.advanced": "למתקדמים", + "home.column_settings.basic": "למתחילים", + "home.column_settings.filter_regex": "סינון באמצעות ביטויים רגולריים (regular expressions)", + "home.column_settings.show_reblogs": "הצגת הדהודים", + "home.column_settings.show_replies": "הצגת תגובות", + "home.settings": "הגדרות טור", + "lightbox.close": "סגירה", + "loading_indicator.label": "טוען...", + "media_gallery.toggle_visible": "נראה\\בלתי נראה", + "missing_indicator.label": "לא נמצא", + "navigation_bar.blocks": "חסימות", + "navigation_bar.community_timeline": "פיד מקומי", + "navigation_bar.edit_profile": "עריכת פרופיל", + "navigation_bar.favourites": "חיבובים", + "navigation_bar.follow_requests": "בקשות מעקב", + "navigation_bar.info": "מידע נוסף", + "navigation_bar.logout": "יציאה", + "navigation_bar.mutes": "השתקות", + "navigation_bar.preferences": "העדפות", + "navigation_bar.public_timeline": "בפרהסיה", + "notification.favourite": "חצרוצך חובב על ידי {name}", + "notification.follow": "{name} במעקב אחרייך", + "notification.mention": "אוזכרת ע\"י {name}", + "notification.reblog": "חצרוצך הודהד על ידי {name}", + "notifications.clear": "הסרת התראות", + "notifications.clear_confirmation": "להסיר את כל ההתראות? בטוח?", + "notifications.column_settings.alert": "התראות לשולחן העבודה", + "notifications.column_settings.favourite": "מחובבים:", + "notifications.column_settings.follow": "עוקבים חדשים:", + "notifications.column_settings.mention": "פניות:", + "notifications.column_settings.reblog": "הדהודים:", + "notifications.column_settings.show": "הצגה בטור", + "notifications.column_settings.sound": "שמע מופעל", + "notifications.settings": "הגדרות טור", + "onboarding.done": "יציאה", + "onboarding.next": "הלאה", + "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.", + "onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.", + "onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.", + "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו: קהילות", + "onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}", + "onboarding.page_one.welcome": "ברוכים הבאים למסטודון!", + "onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.", + "onboarding.page_six.almost_done": "כמעט סיימנו...", + "onboarding.page_six.appetoot": "בתותאבון!", + "onboarding.page_six.apps_available": "קיימים {apps} זמינים עבור אנדרואיד, אייפון ופלטפורמות נוספות.", + "onboarding.page_six.github": "מסטודון הוא תוכנה חופשית. ניתן לדווח על באגים, לבקש יכולות, או לתרום לקוד באתר {github}.", + "onboarding.page_six.guidelines": "חוקי הקהילה", + "onboarding.page_six.read_guidelines": "נא לקרוא את {guidelines} של {domain}!", + "onboarding.page_six.various_app": "יישומונים ניידים", + "onboarding.page_three.profile": "ץתחת 'עריכת פרופיל' ניתן להחליף את תמונת הפרופיל שלך, תיאור קצר, והשם המוצג. שם גם ניתן למצוא אפשרויות והעדפות נוספות.", + "onboarding.page_three.search": "בחלונית החיפוש ניתן לחפש אנשים והאשתגים, כמו למשל {illustration} או {introductions}. כדי למצוא מישהו שלא על האינסטנס המקומי, יש להשתמש בכינוי המשתמש המלא.", + "onboarding.page_two.compose": "הודעות כותבים מטור הכתיבה. ניתן לנעלות תמונות, לשנות הגדרות פרטיות, ולהוסיף אזהרות תוכן בעזרת האייקונים שמתחת.", + "onboarding.skip": "לדלג", + "privacy.change": "שינוי פרטיות ההודעה", + "privacy.direct.long": "הצג רק למי שהודעה זו פונה אליו", + "privacy.direct.short": "הודעה ישירה", + "privacy.private.long": "הצג לעוקבים מקומיים בלבד", + "privacy.private.short": "לעוקבים בלבד", + "privacy.public.long": "פרסם בפומבי", + "privacy.public.short": "פומבי", + "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", + "privacy.unlisted.short": "לא לפיד הכללי", + "reply_indicator.cancel": "ביטול", + "report.heading": "דווח חדש", + "report.placeholder": "הערות נוספות", + "report.submit": "שליחה", + "report.target": "דיווח", + "search.placeholder": "חיפוש", + "search.status_by": "הודעה מאת {name}", + "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", + "status.cannot_reblog": "לא ניתן להדהד הודעה זו", + "status.delete": "מחיקה", + "status.favourite": "חיבוב", + "status.load_more": "עוד", + "status.media_hidden": "מדיה מוסתרת", + "status.mention": "פניה אל @{name}", + "status.open": "הרחבת הודעה", + "status.reblog": "הדהוד", + "status.reblogged_by": "הודהד על ידי {name}", + "status.reply": "תגובה", + "status.replyAll": "תגובה לכולם", + "status.report": "דיווח על @{name}", + "status.sensitive_warning": "תוכן רגיש", + "status.sensitive_toggle": "לחצו כדי לראות", + "status.show_less": "הראה פחות", + "status.show_more": "הראה יותר", + "tabs_bar.compose": "חיבור", + "tabs_bar.federated_timeline": "בפדרציה", + "tabs_bar.home": "בבית", + "tabs_bar.local_timeline": "פיד מקומי", + "tabs_bar.notifications": "התראות", + "upload_area.title": "ניתן להעלות על ידי Drag & drop", + "upload_button.label": "הוספת מדיה", + "upload_form.undo": "ביטול", + "upload_progress.label": "עולה...", + "video_player.expand": "הרחבת וידאו", + "video_player.toggle_sound": "הפעלת\\ביטול שמע", + "video_player.toggle_visible": "הפעלת\\ביטול תצוגה", + "video_player.video_error": "לא ניתן לנגן וידאו" +} diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blokiraj @{name}", + "account.disclaimer": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.", + "account.edit_profile": "Uredi profil", + "account.follow": "Slijedi", + "account.followers": "Sljedbenici", + "account.follows": "Slijedi", + "account.follows_you": "te slijedi", + "account.mention": "Spomeni @{name}", + "account.mute": "Utišaj @{name}", + "account.posts": "Postovi", + "account.report": "Prijavi @{name}", + "account.requested": "Čeka pristanak", + "account.unblock": "Deblokiraj @{name}", + "account.unfollow": "Prestani slijediti", + "account.unmute": "Poništi utišavanje @{name}", + "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", + "column.blocks": "Blokirani korisnici", + "column.community": "Lokalni timeline", + "column.favourites": "Favoriti", + "column.follow_requests": "Zahtjevi za slijeđenje", + "column.home": "Dom", + "column.mutes": "Muted users", + "column.notifications": "Notifikacije", + "column.public": "Federalni timeline", + "column_back_button.label": "Natrag", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Što ti je na umu?", + "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bit biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Označi media sadržaj kao osjetljiv", + "compose_form.spoiler": "Sakrij text iza upozorenja", + "compose_form.spoiler_placeholder": "Upozorenje o sadržaju", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Umetni smajlije", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", + "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", + "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", + "empty_column.home.public_timeline": "javni timeline", + "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", + "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", + "follow_request.authorize": "Authoriziraj", + "follow_request.reject": "Odbij", + "getting_started.apps": "Dostupne su razne aplikacije", + "getting_started.heading": "Počnimo", + "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}. {apps}.", + "home.column_settings.advanced": "Napredno", + "home.column_settings.basic": "Osnovno", + "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima", + "home.column_settings.show_reblogs": "Pokaži boosts", + "home.column_settings.show_replies": "Pokaži odgovore", + "home.settings": "Postavke Stupca", + "lightbox.close": "Zatvori", + "loading_indicator.label": "Učitavam...", + "media_gallery.toggle_visible": "Preklopi vidljivost", + "missing_indicator.label": "Nije nađen", + "navigation_bar.blocks": "Blokirani korisnici", + "navigation_bar.community_timeline": "Lokalni timeline", + "navigation_bar.edit_profile": "Uredi profil", + "navigation_bar.favourites": "Favoriti", + "navigation_bar.follow_requests": "Zahtjevi za sljeđenje", + "navigation_bar.info": "Proširena informacija", + "navigation_bar.logout": "Odjavi se", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Postavke", + "navigation_bar.public_timeline": "Federalni timeline", + "notification.favourite": "{name} je lajkao tvoj status", + "notification.follow": "{name} te sada slijedi", + "notification.reblog": "{name} je podigao tvoj status", + "notifications.clear": "Očisti notifikacije", + "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?", + "notifications.column_settings.alert": "Desktop notifikacije", + "notifications.column_settings.favourite": "Favoriti:", + "notifications.column_settings.follow": "Novi sljedbenici:", + "notifications.column_settings.mention": "Spominjanja:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Prikaži u stupcu", + "notifications.column_settings.sound": "Sviraj zvuk", + "notifications.settings": "Postavke rubrike", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Podesi status privatnosti", + "privacy.direct.long": "Prikaži samo spomenutim korisnicima", + "privacy.direct.short": "Direktno", + "privacy.private.long": "Prikaži samo sljedbenicima", + "privacy.private.short": "Privatno", + "privacy.public.long": "Postaj na javne timeline", + "privacy.public.short": "Javno", + "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Otkaži", + "report.heading": "Nova prijava", + "report.placeholder": "Dodatni komentari", + "report.submit": "Pošalji", + "report.target": "Prijavljivanje", + "search.placeholder": "Traži", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Obriši", + "status.favourite": "Označi omiljenim", + "status.load_more": "Učitaj više", + "status.media_hidden": "Sakriven media sadržaj", + "status.mention": "Spomeni @{name}", + "status.open": "Proširi ovaj status", + "status.reblog": "Podigni", + "status.reblogged_by": "{name} je podigao", + "status.reply": "Odgovori", + "status.replyAll": "Reply to thread", + "status.report": "Prijavi @{name}", + "status.sensitive_toggle": "Klikni da bi vidio", + "status.sensitive_warning": "Osjetljiv sadržaj", + "status.show_less": "Pokaži manje", + "status.show_more": "Pokaži više", + "tabs_bar.compose": "Sastavi", + "tabs_bar.federated_timeline": "Federalni", + "tabs_bar.home": "Dom", + "tabs_bar.local_timeline": "Lokalno", + "tabs_bar.notifications": "Notifikacije", + "upload_area.title": "Povuci & spusti kako bi uploadao", + "upload_button.label": "Dodaj media", + "upload_form.undo": "Poništi", + "upload_progress.label": "Uploadam...", + "video_player.expand": "Proširi video", + "video_player.toggle_sound": "Toggle zvuk", + "video_player.toggle_visible": "Preklopi vidljivost", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blokkolás", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Profil szerkesztése", + "account.follow": "Követés", + "account.followers": "Követők", + "account.follows": "Követve", + "account.follows_you": "Követnek téged", + "account.mention": "Említés", + "account.mute": "Mute @{name}", + "account.posts": "Posts", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.unblock": "Blokkolás levétele", + "account.unfollow": "Követés abbahagyása", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Kezdőlap", + "column.mutes": "Muted users", + "column.notifications": "Értesítések", + "column.public": "Nyilvános", + "column_back_button.label": "Vissza", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Mire gondolsz?", + "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", + "compose_form.publish": "Tülk!", + "compose_form.sensitive": "Tartalom érzékenynek jelölése", + "compose_form.spoiler": "Hide text behind warning", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Első lépések", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Bezárás", + "loading_indicator.label": "Betöltés...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.edit_profile": "Profil szerkesztése", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Kijelentkezés", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Beállítások", + "navigation_bar.public_timeline": "Nyilvános időfolyam", + "notification.favourite": "{name} kedvencnek jelölte az állapotod", + "notification.follow": "{name} követ téged", + "notification.reblog": "{name} reblogolta az állapotod", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Mégsem", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Keresés", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Törlés", + "status.favourite": "Kedvenc", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Említés", + "status.open": "Expand this status", + "status.reblog": "Reblog", + "status.reblogged_by": "{name} reblogolta", + "status.reply": "Válasz", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Katt a megtekintéshez", + "status.sensitive_warning": "Érzékeny tartalom", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Összeállítás", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Kezdőlap", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Média hozzáadása", + "upload_form.undo": "Mégsem", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Hang kapcsolása", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json @@ -0,0 +1,167 @@ +{ + "account.block": "Blokir @{name}", + "account.disclaimer": "Pengguna ini berasal dari server lain. Angka berikut mungkin lebih besar.", + "account.edit_profile": "Ubah profil", + "account.follow": "Ikuti", + "account.followers": "Pengikut", + "account.follows": "Mengikuti", + "account.follows_you": "Mengikuti anda", + "account.mention": "Balasan @{name}", + "account.mute": "Bisukan @{name}", + "account.posts": "Postingan", + "account.report": "Laporkan @{name}", + "account.requested": "Menunggu persetujuan", + "account.unblock": "Hapus blokir @{name}", + "account.unfollow": "Berhenti mengikuti", + "account.unmute": "Berhenti membisukan @{name}", + "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", + "column.blocks": "Pengguna diblokir", + "column.community": "Linimasa Lokal", + "column.favourites": "Favorit", + "column.follow_requests": "Permintaan mengikuti", + "column.home": "Beranda", + "column.mutes": "Pengguna dibisukan", + "column.notifications": "Notifikasi", + "column.public": "Linimasa gabunggan", + "column_back_button.label": "Kembali", + "column_subheading.navigation": "Navigasi", + "column_subheading.settings": "Pengaturan", + "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", + "compose_form.lock_disclaimer.lock": "dikunci", + "compose_form.placeholder": "Apa yang ada di pikiran anda?", + "compose_form.privacy_disclaimer": "Status pribadi anda akan dikirim ke pengguna yang disebut dalam {domains}. Apa anda mempercayai {domainsCount, plural, one {server tersebut} other {server tersebut}}? Privasi postingan hanya bekerja dalam server Mastodon. Jika {domains} {domainsCount, plural, one {bukan server Mastodon} other {bukan server Mastodon}}, akan ada indikasi bahwa postingan anda adalah postingan pribadi, dan dapat di-boost atau dapat dilihat oleh orang lain.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Tandai media sensitif", + "compose_form.spoiler": "Sembunyikan teks dibalik peringatan", + "compose_form.spoiler_placeholder": "Peringatan konten", + "confirmation_modal.cancel": "Batal", + "confirmations.block.confirm": "Blokir", + "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?", + "confirmations.delete.confirm": "Hapus", + "confirmations.delete.message": "Apa anda yakin akan menghapus status ini?", + "confirmations.mute.confirm": "Bisukan", + "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", + "emoji_button.activity": "Aktivitas", + "emoji_button.flags": "Bendera", + "emoji_button.food": "Makanan & Minuman", + "emoji_button.label": "Tambahkan emoji", + "emoji_button.nature": "Alam", + "emoji_button.objects": "Benda-benda", + "emoji_button.people": "Orang", + "emoji_button.search": "Cari...", + "emoji_button.symbols": "Simbol", + "emoji_button.travel": "Tempat Wisata", + "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", + "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", + "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", + "empty_column.home.public_timeline": "linimasa publik", + "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", + "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual", + "follow_request.authorize": "Izinkan", + "follow_request.reject": "Tolak", + "getting_started.apps": "Tersedia dalam berbagai aplikasi", + "getting_started.heading": "Mulai", + "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat open source. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}. {apps}.", + "home.column_settings.advanced": "Tingkat Lanjut", + "home.column_settings.basic": "Dasar", + "home.column_settings.filter_regex": "Penyaringan dengan Regular Expression", + "home.column_settings.show_reblogs": "Tampilkan Boost", + "home.column_settings.show_replies": "Tampilkan balasan", + "home.settings": "Pengaturan kolom", + "lightbox.close": "Tutup", + "loading_indicator.label": "Tunggu sebentar...", + "media_gallery.toggle_visible": "Tampil/Sembunyikan", + "missing_indicator.label": "Tidak ditemukan", + "navigation_bar.blocks": "Pengguna diblokir", + "navigation_bar.community_timeline": "Linimasa lokal", + "navigation_bar.edit_profile": "Ubah profil", + "navigation_bar.favourites": "Favorit", + "navigation_bar.follow_requests": "Permintaan mengikuti", + "navigation_bar.info": "Informasi selengkapnya", + "navigation_bar.logout": "Keluar", + "navigation_bar.mutes": "Pengguna dibisukan", + "navigation_bar.preferences": "Pengaturan", + "navigation_bar.public_timeline": "Linimasa gabungan", + "notification.favourite": "{name} menyukai status anda", + "notification.follow": "{name} mengikuti anda", + "notification.reblog": "{name} mem-boost status anda", + "notifications.clear": "Hapus notifikasi", + "notifications.clear_confirmation": "Apa anda yakin hendak menghapus semua notifikasi anda?", + "notifications.column_settings.alert": "Notifikasi desktop", + "notifications.column_settings.favourite": "Favorit:", + "notifications.column_settings.follow": "Pengikut baru:", + "notifications.column_settings.mention": "Balasan:", + "notifications.column_settings.reblog": "Boost:", + "notifications.column_settings.show": "Tampilkan dalam kolom", + "notifications.column_settings.sound": "Mainkan suara", + "notifications.settings": "Pengaturan kolom", + "onboarding.done": "Selesei", + "onboarding.next": "Selanjutnya", + "onboarding.page_five.public_timelines": "Linimasa lokal menampilkan semua postingan publik dari semua orang di {domain}. Linimasa gabungan menampilkan postingan publik dari semua orang yang diikuti oleh {domain}. Ini semua adalah Linimasa Publik, cara terbaik untuk bertemu orang lain.", + "onboarding.page_four.home": "Linimasa beranda menampilkan postingan dari orang-orang yang anda ikuti.", + "onboarding.page_four.notifications": "Kolom notifikasi menampilkan ketika seseorang berinteraksi dengan anda.", + "onboarding.page_one.federation": "Mastodon adalah jaringan dari beberapa server independen yang bergabung untuk membuat jejaring sosial yang besar.", + "onboarding.page_one.handle": "Ada berada dalam {domain}, jadi nama user lengkap anda adalah {handle}", + "onboarding.page_one.welcome": "Selamat datang di Mastodon!", + "onboarding.page_six.admin": "Admin serveer anda adalah {admin}.", + "onboarding.page_six.almost_done": "Hampir selesei...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "Ada beberapa apl yang tersedia untuk iOS, Android, dan platform lainnya.", + "onboarding.page_six.github": "Mastodon adalah software open-source. Anda bisa melaporkan bug, meminta fitur, atau berkontribusi dengan kode di {github}.", + "onboarding.page_six.guidelines": "pedoman komunitas", + "onboarding.page_six.read_guidelines": "Silakan baca {guidelines} {domain}!", + "onboarding.page_six.various_app": "apl handphone", + "onboarding.page_three.profile": "Ubah profil anda untuk mengganti avatar, bio, dan nama pengguna anda. Disitu, anda juga bisa mengatur opsi lainnya.", + "onboarding.page_three.search": "Gunakan kolom pencarian untuk mencari orang atau melihat hashtag, seperti {illustration} dan {introductions}. Untuk mencari pengguna yang tidak berada dalam server ini, gunakan nama pengguna mereka selengkapnya.", + "onboarding.page_two.compose": "Tulis postingan melalui kolom posting. Anda dapat mengunggah gambar, mengganti pengaturan privasi, dan menambahkan peringatan konten dengan ikon-ikon dibawah ini.", + "onboarding.skip": "Lewati", + "privacy.change": "Tentukan privasi status", + "privacy.direct.long": "Kirim hanya ke pengguna yang disebut", + "privacy.direct.short": "Langsung", + "privacy.private.long": "Kirim hanya ke pengikut", + "privacy.private.short": "Pribadi", + "privacy.public.long": "Kirim ke linimasa publik", + "privacy.public.short": "Publik", + "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", + "privacy.unlisted.short": "Tak Terdaftar", + "reply_indicator.cancel": "Batal", + "report.heading": "Laporan baru", + "report.placeholder": "Komentar tambahan", + "report.submit": "Kirim", + "report.target": "Melaporkan", + "search.status_by": "Status yang dibuat oleh {name}", + "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", + "status.cannot_reblog": "Postingan ini tidak dapat di-boost", + "search.placeholder": "Pencarian", + "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Hapus", + "status.favourite": "Difavoritkan", + "status.load_more": "Tampilkan semua", + "status.media_hidden": "Media disembunyikan", + "status.mention": "Balasan @{name}", + "status.open": "Tampilkan status ini", + "status.reblog": "Boost", + "status.reblogged_by": "di-boost {name}", + "status.reply": "Balas", + "status.replyAll": "Balas ke semua", + "status.report": "Laporkan @{name}", + "status.sensitive_toggle": "Klik untuk menampilkan", + "status.sensitive_warning": "Konten sensitif", + "status.show_less": "Tampilkan lebih sedikit", + "status.show_more": "Tampilkan semua", + "tabs_bar.compose": "Tulis", + "tabs_bar.federated_timeline": "Gabungan", + "tabs_bar.home": "Beranda", + "tabs_bar.local_timeline": "Lokal", + "tabs_bar.notifications": "Notifikasi", + "upload_area.title": "Seret & lepaskan untuk mengunggah", + "upload_button.label": "Tambahkan media", + "upload_form.undo": "Undo", + "upload_progress.label": "Mengunggah...", + "video_player.expand": "Tampilkan video", + "video_player.toggle_sound": "Suara", + "video_player.toggle_visible": "Tampilan", + "video_player.expand": "Tampilkan video", + "video_player.video_error": "Video tidak dapat diputar" +} diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js @@ -0,0 +1,57 @@ +import ar from './ar.json'; +import en from './en.json'; +import de from './de.json'; +import es from './es.json'; +import fa from './fa.json'; +import he from './he.json'; +import hr from './hr.json'; +import hu from './hu.json'; +import io from './io.json'; +import it from './it.json'; +import fr from './fr.json'; +import nl from './nl.json'; +import no from './no.json'; +import oc from './oc.json'; +import pt from './pt.json'; +import pt_br from './pt-BR.json'; +import uk from './uk.json'; +import fi from './fi.json'; +import eo from './eo.json'; +import ru from './ru.json'; +import ja from './ja.json'; +import zh_hk from './zh-HK.json'; +import zh_cn from './zh-CN.json'; +import bg from './bg.json'; +import id from './id.json'; + +const locales = { + ar, + en, + de, + es, + fa, + he, + hr, + hu, + io, + it, + fr, + nl, + no, + oc, + pt, + 'pt-BR': pt_br, + uk, + fi, + eo, + ru, + ja, + 'zh-HK': zh_hk, + 'zh-CN': zh_cn, + bg, + id, +}; + +export default function getMessagesForLocale(locale) { + return locales[locale]; +}; diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blokusar @{name}", + "account.disclaimer": "Ca uzero esas de altra instaluro. Ca nombro forsan esas plu granda.", + "account.edit_profile": "Modifikar profilo", + "account.follow": "Sequar", + "account.followers": "Sequanti", + "account.follows": "Sequas", + "account.follows_you": "Sequas tu", + "account.mention": "Mencionar @{name}", + "account.mute": "Celar @{name}", + "account.posts": "Mesaji", + "account.report": "Denuncar @{name}", + "account.requested": "Vartante aprobo", + "account.unblock": "Desblokusar @{name}", + "account.unfollow": "Ne plus sequar", + "account.unmute": "Ne plus celar @{name}", + "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", + "column.blocks": "Blokusita uzeri", + "column.community": "Lokala tempolineo", + "column.favourites": "Favorati", + "column.follow_requests": "Demandi di sequado", + "column.home": "Hemo", + "column.mutes": "Celita uzeri", + "column.notifications": "Savigi", + "column.public": "Federata tempolineo", + "column_back_button.label": "Retro", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Quo esas en tua spirito?", + "compose_form.privacy_disclaimer": "Tua privata mesajo livresos a mencionata uzeri en {domains}. Ka tu fidas {domainsCount, plural, one {ta servero} other {ta serveri}}? Privateso di mesaji funcionas nur en instaluri di Mastodon. Se {domains} {domainsCount, plural, one {ne esas instaluro di Mastodon} other {ne esas instaluri di Mastodon}}, esos nula indiko, ke tua mesajo esas privata, ed ol povos repetesar od altre divenar videbla da nedezirinda recevanti.", + "compose_form.publish": "Siflar", + "compose_form.sensitive": "Markizar kontenajo kom trubliva", + "compose_form.spoiler": "Celar texto dop averto", + "compose_form.spoiler_placeholder": "Averto di kontenajo", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insertar emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!", + "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.", + "empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.", + "empty_column.home.public_timeline": "la publika tempolineo", + "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.", + "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.", + "follow_request.authorize": "Yurizar", + "follow_request.reject": "Refuzar", + "getting_started.apps": "Apliki diversa esas disponebla", + "getting_started.heading": "Debuto", + "getting_started.open_source_notice": "Mastodon esas programaro kun apertita kodexo. Tu povas kontributar o signalar problemi en GitHub ye {github}. {apps}.", + "home.column_settings.advanced": "Komplexa", + "home.column_settings.basic": "Simpla", + "home.column_settings.filter_regex": "Ekfiltrar per reguloza expresuri", + "home.column_settings.show_reblogs": "Montrar repeti", + "home.column_settings.show_replies": "Montrar respondi", + "home.settings": "Aranji di la kolumno", + "lightbox.close": "Klozar", + "loading_indicator.label": "Kargante...", + "media_gallery.toggle_visible": "Chanjar videbleso", + "missing_indicator.label": "Ne trovita", + "navigation_bar.blocks": "Blokusita uzeri", + "navigation_bar.community_timeline": "Lokala tempolineo", + "navigation_bar.edit_profile": "Modifikar profilo", + "navigation_bar.favourites": "Favorati", + "navigation_bar.follow_requests": "Demandi di sequado", + "navigation_bar.info": "Detaloza informi", + "navigation_bar.logout": "Ekirar", + "navigation_bar.mutes": "Celita uzeri", + "navigation_bar.preferences": "Preferi", + "navigation_bar.public_timeline": "Federata tempolineo", + "notification.favourite": "{name} favorizis tua mesajo", + "notification.follow": "{name} sequeskis tu", + "notification.reblog": "{name} repetis tua mesajo", + "notifications.clear": "Efacar savigi", + "notifications.clear_confirmation": "Ka tu esas certa, ke tu volas efacar omna tua savigi?", + "notifications.column_settings.alert": "Surtabla savigi", + "notifications.column_settings.favourite": "Favorati:", + "notifications.column_settings.follow": "Nova sequanti:", + "notifications.column_settings.mention": "Mencioni:", + "notifications.column_settings.reblog": "Repeti:", + "notifications.column_settings.show": "Montrar en kolumno", + "notifications.column_settings.sound": "Plear sono", + "notifications.settings": "Aranji di kolumno", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Aranjar privateso di mesaji", + "privacy.direct.long": "Sendar nur a mencionata uzeri", + "privacy.direct.short": "Direte", + "privacy.private.long": "Sendar nur a sequanti", + "privacy.private.short": "Private", + "privacy.public.long": "Sendar a publika tempolinei", + "privacy.public.short": "Publike", + "privacy.unlisted.long": "Ne montrar en publika tempolinei", + "privacy.unlisted.short": "Ne enlistigota", + "reply_indicator.cancel": "Nihiligar", + "report.heading": "Nova denunco", + "report.placeholder": "Plusa komenti", + "report.submit": "Sendar", + "report.target": "Denuncante", + "search.placeholder": "Serchez", + "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Efacar", + "status.favourite": "Favorizar", + "status.load_more": "Kargar pluse", + "status.media_hidden": "Kontenajo celita", + "status.mention": "Mencionar @{name}", + "status.open": "Detaligar ca mesajo", + "status.reblog": "Repetar", + "status.reblogged_by": "{name} repetita", + "status.reply": "Respondar", + "status.replyAll": "Respondar a filo", + "status.report": "Denuncar @{name}", + "status.sensitive_toggle": "Kliktar por vidar", + "status.sensitive_warning": "Trubliva kontenajo", + "status.show_less": "Montrar mine", + "status.show_more": "Montrar plue", + "tabs_bar.compose": "Kompozar", + "tabs_bar.federated_timeline": "Federata", + "tabs_bar.home": "Hemo", + "tabs_bar.local_timeline": "Lokala", + "tabs_bar.notifications": "Savigi", + "upload_area.title": "Tranar faligar por kargar", + "upload_button.label": "Adjuntar kontenajo", + "upload_form.undo": "Desfacar", + "upload_progress.label": "Kargante...", + "video_player.expand": "Extensar video", + "video_player.toggle_sound": "Acendar sono", + "video_player.toggle_visible": "Chanjar videbleso", + "video_player.video_error": "Video ne povus pleesar" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blocca @{name}", + "account.disclaimer": "Questo utente si trova su un altro server. Questo numero potrebbe essere maggiore.", + "account.edit_profile": "Modifica profilo", + "account.follow": "Segui", + "account.followers": "Seguaci", + "account.follows": "Segue", + "account.follows_you": "Ti segue", + "account.mention": "Menziona @{name}", + "account.mute": "Silenzia @{name}", + "account.posts": "Posts", + "account.report": "Segnala @{name}", + "account.requested": "In attesa di approvazione", + "account.unblock": "Sblocca @{name}", + "account.unfollow": "Non seguire", + "account.unmute": "Non silenziare @{name}", + "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", + "column.blocks": "Utenti bloccati", + "column.community": "Timeline locale", + "column.favourites": "Apprezzati", + "column.follow_requests": "Richieste di amicizia", + "column.home": "Home", + "column.mutes": "Utenti silenziati", + "column.notifications": "Notifiche", + "column.public": "Timeline federata", + "column_back_button.label": "Indietro", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "A cosa stai pensando?", + "compose_form.privacy_disclaimer": "Il tuo status privato verrà condiviso con gli utenti menzionati su {domains}. Ti fidi di {domainsCount, plural, one {quel server} other {quei server}}? Le impostazioni sulla privacy valgono solo su server Mastodon. Se {domains} {domainsCount, plural, one {non è un server Mastodon} other {non sono server Mastodon}}, non ci saranno indicazioni sulla privacy del tuo status, e potrebbe essere condiviso o reso visibile a destinatari indesiderati.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Segnala file come sensibile", + "compose_form.spoiler": "Nascondi testo con avvertimento", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Inserisci emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", + "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.", + "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.", + "empty_column.home.public_timeline": "la timeline pubblica", + "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.", + "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.", + "follow_request.authorize": "Autorizza", + "follow_request.reject": "Rifiuta", + "getting_started.apps": "Sono disponibili diverse app", + "getting_started.heading": "Come iniziare", + "getting_started.open_source_notice": "Mastodon è un software open source. Puoi contribuire o segnalare errori su GitHub all'indirizzo {github}. {apps}.", + "home.column_settings.advanced": "Avanzato", + "home.column_settings.basic": "Semplice", + "home.column_settings.filter_regex": "Filtra con espressioni regolari", + "home.column_settings.show_reblogs": "Mostra post condivisi", + "home.column_settings.show_replies": "Mostra risposte", + "home.settings": "Impostazioni colonna", + "lightbox.close": "Chiudi", + "loading_indicator.label": "Carico...", + "media_gallery.toggle_visible": "Imposta visibilità", + "missing_indicator.label": "Non trovato", + "navigation_bar.blocks": "Utenti bloccati", + "navigation_bar.community_timeline": "Timeline locale", + "navigation_bar.edit_profile": "Modifica profilo", + "navigation_bar.favourites": "Apprezzati", + "navigation_bar.follow_requests": "Richieste di amicizia", + "navigation_bar.info": "Informazioni estese", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Utenti silenziati", + "navigation_bar.preferences": "Impostazioni", + "navigation_bar.public_timeline": "Timeline federata", + "notification.favourite": "{name} ha apprezzato il tuo post", + "notification.follow": "{name} ha iniziato a seguirti", + "notification.reblog": "{name} ha condiviso il tuo post", + "notifications.clear": "Cancella notifiche", + "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?", + "notifications.column_settings.alert": "Notifiche desktop", + "notifications.column_settings.favourite": "Apprezzati:", + "notifications.column_settings.follow": "Nuovi seguaci:", + "notifications.column_settings.mention": "Menzioni:", + "notifications.column_settings.reblog": "Post condivisi:", + "notifications.column_settings.show": "Mostra in colonna", + "notifications.column_settings.sound": "Riproduci suono", + "notifications.settings": "Impostazioni colonna", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Modifica privacy post", + "privacy.direct.long": "Invia solo a utenti menzionati", + "privacy.direct.short": "Diretto", + "privacy.private.long": "Invia solo ai seguaci", + "privacy.private.short": "Privato", + "privacy.public.long": "Invia alla timeline pubblica", + "privacy.public.short": "Pubblico", + "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", + "privacy.unlisted.short": "Non elencato", + "reply_indicator.cancel": "Annulla", + "report.heading": "Nuova segnalazione", + "report.placeholder": "Commenti aggiuntivi", + "report.submit": "Invia", + "report.target": "Invio la segnalazione", + "search.placeholder": "Cerca", + "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Elimina", + "status.favourite": "Apprezzato", + "status.load_more": "Mostra di più", + "status.media_hidden": "Allegato nascosto", + "status.mention": "Nomina @{name}", + "status.open": "Espandi questo post", + "status.reblog": "Condividi", + "status.reblogged_by": "{name} ha condiviso", + "status.reply": "Rispondi", + "status.replyAll": "Reply to thread", + "status.report": "Segnala @{name}", + "status.sensitive_toggle": "Clicca per vedere", + "status.sensitive_warning": "Materiale sensibile", + "status.show_less": "Mostra meno", + "status.show_more": "Mostra di più", + "tabs_bar.compose": "Scrivi", + "tabs_bar.federated_timeline": "Federazione", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Locale", + "tabs_bar.notifications": "Notifiche", + "upload_area.title": "Trascina per caricare", + "upload_button.label": "Aggiungi file multimediale", + "upload_form.undo": "Annulla", + "upload_progress.label": "Sto caricando...", + "video_player.expand": "Espandi video", + "video_player.toggle_sound": "Attiva suono", + "video_player.toggle_visible": "Attiva visibilità", + "video_player.video_error": "Il video non può essere riprodotto" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json @@ -0,0 +1,163 @@ +{ + "account.block": "ブロック", + "account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。", + "account.edit_profile": "プロフィールを編集", + "account.follow": "フォロー", + "account.followers": "フォロワー", + "account.follows": "フォロー", + "account.follows_you": "フォローされています", + "account.mention": "返信", + "account.mute": "ミュート", + "account.posts": "投稿", + "account.report": "通報", + "account.requested": "承認待ち", + "account.unblock": "ブロック解除", + "account.unfollow": "フォロー解除", + "account.unmute": "ミュート解除", + "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", + "column.blocks": "ブロックしたユーザー", + "column.community": "ローカルタイムライン", + "column.favourites": "お気に入り", + "column.follow_requests": "フォローリクエスト", + "column.home": "ホーム", + "column.mutes": "ミュートしたユーザー", + "column.notifications": "通知", + "column.public": "連合タイムライン", + "column_back_button.label": "戻る", + "column_subheading.navigation": "ナビゲーション", + "column_subheading.settings": "設定", + "compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。", + "compose_form.lock_disclaimer.lock": "非公開", + "compose_form.placeholder": "今なにしてる?", + "compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先ユーザーが所属する {domains} に送信されます。{domainsCount, plural, one {このサーバー} other {これらのサーバー}}は信頼できますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 {domains} {domainsCount, plural, one {がMastodonインスタンス} other {がMastodonインスタンス}}でない場合、あなたの投稿がプライベートなものとして扱われず、ブーストされたり予期しないユーザーに見られる可能性があります。", + "compose_form.publish": "トゥート", + "compose_form.sensitive": "メディアを閲覧注意としてマークする", + "compose_form.spoiler": "テキストを隠す", + "compose_form.spoiler_placeholder": "警告", + "confirmation_modal.cancel": "キャンセル", + "confirmations.block.confirm": "ブロック", + "confirmations.block.message": "本当に {name} をブロックしますか?", + "confirmations.delete.confirm": "削除", + "confirmations.delete.message": "本当に削除しますか?", + "confirmations.mute.confirm": "ミュート", + "confirmations.mute.message": "本当に {name} をミュートしますか?", + "emoji_button.activity": "活動", + "emoji_button.flags": "国旗", + "emoji_button.food": "食べ物", + "emoji_button.label": "絵文字を追加", + "emoji_button.nature": "自然", + "emoji_button.objects": "物", + "emoji_button.people": "人々", + "emoji_button.search": "検索...", + "emoji_button.symbols": "記号", + "emoji_button.travel": "旅行と場所", + "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!", + "empty_column.hashtag": "このハッシュタグはまだ使われていません。", + "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。", + "empty_column.home.public_timeline": "連合タイムライン", + "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", + "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!", + "follow_request.authorize": "許可", + "follow_request.reject": "拒否", + "getting_started.apps": "さまざまなアプリで利用できます。", + "getting_started.heading": "スタート", + "getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}", + "home.column_settings.advanced": "上級者向け", + "home.column_settings.basic": "シンプル", + "home.column_settings.filter_regex": "正規表現でフィルター", + "home.column_settings.show_reblogs": "ブースト表示", + "home.column_settings.show_replies": "返信表示", + "home.settings": "カラム設定", + "lightbox.close": "閉じる", + "loading_indicator.label": "読み込み中...", + "media_gallery.toggle_visible": "表示切り替え", + "missing_indicator.label": "見つかりません", + "navigation_bar.blocks": "ブロックしたユーザー", + "navigation_bar.community_timeline": "ローカルタイムライン", + "navigation_bar.edit_profile": "プロフィールを編集", + "navigation_bar.favourites": "お気に入り", + "navigation_bar.follow_requests": "フォローリクエスト", + "navigation_bar.info": "サーバー情報", + "navigation_bar.logout": "ログアウト", + "navigation_bar.mutes": "ミュートしたユーザー", + "navigation_bar.preferences": "ユーザー設定", + "navigation_bar.public_timeline": "連合タイムライン", + "notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました", + "notification.follow": "{name} さんにフォローされました", + "notification.reblog": "{name} さんがあなたのトゥートをブーストしました", + "notifications.clear": "通知を消去", + "notifications.clear_confirmation": "本当に通知を消去しますか?", + "notifications.column_settings.alert": "デスクトップ通知", + "notifications.column_settings.favourite": "お気に入り", + "notifications.column_settings.follow": "新しいフォロワー", + "notifications.column_settings.mention": "返信", + "notifications.column_settings.reblog": "ブースト", + "notifications.column_settings.show": "カラムに表示", + "notifications.column_settings.sound": "通知音を再生", + "notifications.settings": "カラム設定", + "onboarding.done": "完了", + "onboarding.next": "次へ", + "onboarding.page_five.public_timelines": "連合タイムラインでは{domain}の人がフォローしているMastodon全体での公開投稿を表示します。同じくローカルタイムラインでは{domain}のみの公開投稿を表示します。", + "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。", + "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。", + "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。", + "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。", + "onboarding.page_one.welcome": "Mastodonへようこそ!", + "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。", + "onboarding.page_six.almost_done": "以上です。", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。", + "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。", + "onboarding.page_six.guidelines": "コミュニティガイドライン", + "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。", + "onboarding.page_six.various_app": "様々なモバイルアプリ", + "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。", + "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。", + "onboarding.page_two.compose": "フォームから投稿できます。イメージや、公開範囲の設定や、表示時の警告の設定は下部のアイコンから行なえます。", + "onboarding.skip": "スキップ", + "privacy.change": "投稿のプライバシーを変更", + "privacy.direct.long": "メンションしたユーザーだけに公開", + "privacy.direct.short": "ダイレクト", + "privacy.private.long": "フォロワーだけに公開", + "privacy.private.short": "非公開", + "privacy.public.long": "公開TLに投稿する", + "privacy.public.short": "公開", + "privacy.unlisted.long": "公開TLで表示しない", + "privacy.unlisted.short": "未収載", + "reply_indicator.cancel": "キャンセル", + "report.heading": "新規通報", + "report.placeholder": "コメント", + "report.submit": "通報する", + "report.target": "問題のユーザー", + "search.placeholder": "検索", + "search_results.total": "{count, number} 件の結果", + "status.cannot_reblog": "この投稿はブーストできません", + "status.delete": "削除", + "status.favourite": "お気に入り", + "status.load_more": "もっと見る", + "status.media_hidden": "非表示のメデイア", + "status.mention": "返信", + "status.open": "詳細を表示", + "status.reblog": "ブースト", + "status.reblogged_by": "{name} さんにブーストされました", + "status.reply": "返信", + "status.replyAll": "全員に返信", + "status.report": "通報", + "status.sensitive_toggle": "クリックして表示", + "status.sensitive_warning": "閲覧注意", + "status.show_less": "隠す", + "status.show_more": "もっと見る", + "tabs_bar.compose": "投稿", + "tabs_bar.federated_timeline": "連合", + "tabs_bar.home": "ホーム", + "tabs_bar.local_timeline": "ローカル", + "tabs_bar.notifications": "通知", + "upload_area.title": "ドラッグ&ドロップでアップロード", + "upload_button.label": "メディアを追加", + "upload_form.undo": "やり直す", + "upload_progress.label": "アップロード中…", + "video_player.expand": "動画の詳細", + "video_player.toggle_sound": "音の切り替え", + "video_player.toggle_visible": "表示切り替え", + "video_player.video_error": "動画の再生に失敗しました" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blokkeer @{name}", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Profiel bewerken", + "account.follow": "Volgen", + "account.followers": "Volgers", + "account.follows": "Volgt", + "account.follows_you": "Volgt jou", + "account.mention": "Vermeld @{name}", + "account.mute": "Negeer @{name}", + "account.posts": "Berichten", + "account.report": "Rapporteer @{name}", + "account.requested": "Wacht op goedkeuring", + "account.unblock": "Deblokkeer @{name}", + "account.unfollow": "Ontvolgen", + "account.unmute": "Negeer @{name} niet meer", + "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", + "column.blocks": "Geblokkeerde gebruikers", + "column.community": "Lokale tijdlijn", + "column.favourites": "Favorieten", + "column.follow_requests": "Follow requests", + "column.home": "Jouw tijdlijn", + "column.mutes": "Genegeerde gebruikers", + "column.notifications": "Meldingen", + "column.public": "Globale tijdlijn", + "column_back_button.label": "terug", + "column_subheading.navigation": "Navigatie", + "column_subheading.settings": "Instellingen", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Wat wil je kwijt?", + "compose_form.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {die server} other {die servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Wanneer {domains} {domainsCount, plural, one {geen Mastodon-server is} other {geen Mastodon-servers zijn}}, dan wordt er niet aangegeven dat de toot privé is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Media als gevoelig markeren", + "compose_form.spoiler": "Tekst achter waarschuwing verbergen", + "compose_form.spoiler_placeholder": "Waarschuwingstekst", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activiteiten", + "emoji_button.flags": "Vlaggen", + "emoji_button.food": "Eten en drinken", + "emoji_button.label": "Emoji toevoegen", + "emoji_button.nature": "Natuur", + "emoji_button.objects": "Voorwerpen", + "emoji_button.people": "Mensen", + "emoji_button.search": "Zoeken...", + "emoji_button.symbols": "Symbolen", + "emoji_button.travel": "Reizen en plekken", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Er zijn meerdere apps beschikbaar", + "getting_started.heading": "Beginnen", + "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Sluiten", + "loading_indicator.label": "Laden…", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Geblokkeerde gebruikers", + "navigation_bar.community_timeline": "Lokale tijdlijn", + "navigation_bar.edit_profile": "Profiel bewerken", + "navigation_bar.favourites": "Favorieten", + "navigation_bar.follow_requests": "Volgverzoeken", + "navigation_bar.info": "Uitgebreide informatie", + "navigation_bar.logout": "Afmelden", + "navigation_bar.mutes": "Genegeerde gebruikers", + "navigation_bar.preferences": "Instellingen", + "navigation_bar.public_timeline": "Globale tijdlijn", + "notification.favourite": "{name} markeerde jouw toot als favoriet", + "notification.follow": "{name} volgt jou nu", + "notification.reblog": "{name} boostte jouw toot", + "notifications.clear": "Meldingen verwijderen", + "notifications.clear_confirmation": "Weet je zeker dat je al jouw meldingen wilt verwijderen?", + "notifications.column_settings.alert": "Desktopmeldingen", + "notifications.column_settings.favourite": "Favorieten:", + "notifications.column_settings.follow": "Nieuwe volgers:", + "notifications.column_settings.mention": "Vermeldingen:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "In kolom tonen", + "notifications.column_settings.sound": "Geluid afspelen", + "notifications.settings": "Kolom-instellingen", + "onboarding.done": "Done", + "onboarding.next": "Volgende", + "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", + "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.", + "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.", + "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.", + "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}", + "onboarding.page_one.welcome": "Welkom op Mastodon!", + "onboarding.page_six.admin": "De beheerder van jouw Mastodon-server is {admin}.", + "onboarding.page_six.almost_done": "Bijna klaar...", + "onboarding.page_six.appetoot": "Veel succes!", + "onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.", + "onboarding.page_six.github": "Mastodon kost niets, en is open-source- en vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.", + "onboarding.page_six.guidelines": "communityrichtlijnen", + "onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!", + "onboarding.page_six.various_app": "mobiele apps", + "onboarding.page_three.profile": "Bewerk jouw profiel om jouw avatar, bio en weergavenaam te veranderen. Daar vind je ook andere instellingen.", + "onboarding.page_three.search": "Gebruik de zoekbalk linksboven om andere mensen op Mastodon te vinden en om te zoeken op hashtags, zoals {illustration} en {introductions}. Om iemand te vinden die niet op deze Mastodon-server zit, moet je het volledige Mastodon-adres van deze persoon invoeren.", + "onboarding.page_two.compose": "Schrijf berichten (wij noemen dit toots) in het tekstvak in de linkerkolom. Je kan met de pictogrammen daaronder afbeeldingen uploaden, privacy-instellingen veranderen en je tekst een waarschuwing meegeven.", + "onboarding.skip": "Overslaan", + "privacy.change": "Privacy toot aanpassen", + "privacy.direct.long": "Toot alleen naar vermelde gebruikers", + "privacy.direct.short": "Direct", + "privacy.private.long": "Alleen aan volgers tonen", + "privacy.private.short": "Alleen volgers", + "privacy.public.long": "Op openbare tijdlijnen tonen", + "privacy.public.short": "Openbaar", + "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", + "privacy.unlisted.short": "Minder openbaar", + "reply_indicator.cancel": "Annuleren", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Zoeken", + "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Verwijderen", + "status.favourite": "Favoriet", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "@{name} vermelden", + "status.open": "Expand this status", + "status.reblog": "Boost", + "status.reblogged_by": "{name} boostte", + "status.reply": "Reageren", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Klik om te zien", + "status.sensitive_warning": "Gevoelige inhoud", + "status.show_less": "Minder tonen", + "status.show_more": "Meer tonen", + "tabs_bar.compose": "Schrijven", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Jouw tijdlijn", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Meldingen", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Media toevoegen", + "upload_form.undo": "Ongedaan maken", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Geluid in-/uitschakelen", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blokkér @{name}", + "account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.", + "account.edit_profile": "Rediger profil", + "account.follow": "Følg", + "account.followers": "Følgere", + "account.follows_you": "Følger deg", + "account.follows": "Følger", + "account.mention": "Nevn @{name}", + "account.mute": "Demp @{name}", + "account.posts": "Innlegg", + "account.report": "Rapportér @{name}", + "account.requested": "Venter på godkjennelse", + "account.unblock": "Avblokker @{name}", + "account.unfollow": "Avfølg", + "account.unmute": "Avdemp @{name}", + "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", + "column.blocks": "Blokkerte brukere", + "column.community": "Lokal tidslinje", + "column.favourites": "Likt", + "column.follow_requests": "Følgeforespørsler", + "column.home": "Hjem", + "column.mutes": "Muted users", + "column.notifications": "Varslinger", + "column.public": "Felles tidslinje", + "column_back_button.label": "Tilbake", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Hva har du på hjertet?", + "compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.", + "compose_form.publish": "Tut", + "compose_form.sensitive": "Merk media som følsomt", + "compose_form.spoiler": "Skjul tekst bak advarsel", + "compose_form.spoiler_placeholder": "Innholdsadvarsel", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Sett inn emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!", + "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.", + "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.", + "empty_column.home.public_timeline": "en offentlig tidslinje", + "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.", + "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp", + "follow_request.authorize": "Autorisér", + "follow_request.reject": "Avvis", + "getting_started.apps": "Diverse apper er tilgjengelige", + "getting_started.heading": "Kom i gang", + "getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.", + "home.column_settings.advanced": "Advansert", + "home.column_settings.basic": "Enkel", + "home.column_settings.filter_regex": "Filtrér med regulære uttrykk", + "home.column_settings.show_reblogs": "Vis fremhevinger", + "home.column_settings.show_replies": "Vis svar", + "home.settings": "Kolonneinnstillinger", + "lightbox.close": "Lukk", + "loading_indicator.label": "Laster...", + "media_gallery.toggle_visible": "Veksle synlighet", + "missing_indicator.label": "Ikke funnet", + "navigation_bar.blocks": "Blokkerte brukere", + "navigation_bar.community_timeline": "Lokal tidslinje", + "navigation_bar.edit_profile": "Rediger profil", + "navigation_bar.favourites": "Likt", + "navigation_bar.follow_requests": "Følgeforespørsler", + "navigation_bar.info": "Utvidet informasjon", + "navigation_bar.logout": "Logg ut", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Preferanser", + "navigation_bar.public_timeline": "Felles tidslinje", + "notification.favourite": "{name} likte din status", + "notification.follow": "{name} fulgte deg", + "notification.reblog": "{name} fremhevde din status", + "notifications.clear": "Fjern varsler", + "notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?", + "notifications.column_settings.alert": "Skrivebordsvarslinger", + "notifications.column_settings.favourite": "Likt:", + "notifications.column_settings.follow": "Nye følgere:", + "notifications.column_settings.mention": "Nevninger:", + "notifications.column_settings.reblog": "Fremhevinger:", + "notifications.column_settings.show": "Vis i kolonne", + "notifications.column_settings.sound": "Spill lyd", + "notifications.settings": "Kolonneinstillinger", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Justér synlighet", + "privacy.direct.long": "Post kun til nevnte brukere", + "privacy.direct.short": "Direkte", + "privacy.private.long": "Post kun til følgere", + "privacy.private.short": "Privat", + "privacy.public.long": "Post kun til offentlige tidslinjer", + "privacy.public.short": "Offentlig", + "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", + "privacy.unlisted.short": "Uoppført", + "reply_indicator.cancel": "Avbryt", + "report.heading": "Ny rapport", + "report.placeholder": "Tilleggskommentarer", + "report.submit": "Send inn", + "report.target": "Rapporterer", + "search.placeholder": "Søk", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Slett", + "status.favourite": "Lik", + "status.load_more": "Last mer", + "status.media_hidden": "Media skjult", + "status.mention": "Nevn @{name}", + "status.open": "Utvid denne statusen", + "status.reblog": "Fremhev", + "status.reblogged_by": "Fremhevd av {name}", + "status.reply": "Svar", + "status.replyAll": "Reply to thread", + "status.report": "Rapporter @{name}", + "status.sensitive_toggle": "Klikk for å vise", + "status.sensitive_warning": "Følsomt innhold", + "status.show_less": "Vis mindre", + "status.show_more": "Vis mer", + "tabs_bar.compose": "Komponer", + "tabs_bar.federated_timeline": "Felles", + "tabs_bar.home": "Hjem", + "tabs_bar.local_timeline": "Lokal", + "tabs_bar.notifications": "Varslinger", + "upload_area.title": "Dra og slipp for å laste opp", + "upload_button.label": "Legg til media", + "upload_form.undo": "Angre", + "upload_progress.label": "Laster opp...", + "video_player.expand": "Utvid video", + "video_player.toggle_sound": "Veksle lyd", + "video_player.toggle_visible": "Veksle synlighet", + "video_player.video_error": "Video could not be played" +} diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json @@ -0,0 +1,163 @@ +{ + "account.block": "Blocar", + "account.disclaimer": "Aqueste compte es sus una autra instància. Los nombres pòdon èsser mai grandes.", + "account.edit_profile": "Modificar lo perfil", + "account.follow": "Sègre", + "account.followers": "Abonats", + "account.follows": "Abonaments", + "account.follows_you": "Vos sèc", + "account.mention": "Mencionar", + "account.mute": "Rescondre", + "account.posts": "Estatuts", + "account.report": "Senhalar", + "account.requested": "Invitacion mandada", + "account.unblock": "Desblocar", + "account.unfollow": "Quitar de sègre", + "account.unmute": "Quitar de rescondre", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Personas blocadas", + "column.community": "Fil public local", + "column.favourites": "Favorits", + "column.follow_requests": "Demandas d’abonament", + "column.home": "Acuèlh", + "column.mutes": "Muted users", + "column.notifications": "Notificacions", + "column.public": "Fil public global", + "column_back_button.label": "Tornar", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "A de qué pensatz ?", + "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", + "compose_form.publish": "Tut", + "compose_form.sensitive": "Marcar lo mèdia coma embarrassant", + "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", + "compose_form.spoiler_placeholder": "Avertiment", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Inserir un emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Lo fil public local es void. Escribètz quicòm per lo garnir !", + "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", + "empty_column.home": "Pel moment segètz pas segun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.", + "empty_column.home.public_timeline": "lo fil public", + "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", + "empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo fil public.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Regetar", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Per començar", + "getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via{github} sus GitHub.", + "home.column_settings.advanced": "Avançat", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filtrar amb una expression racionala", + "home.column_settings.show_reblogs": "Mostrar los partatges", + "home.column_settings.show_replies": "Mostrar las responsas", + "home.settings": "Paramètres de la colomna", + "lightbox.close": "Tampar", + "loading_indicator.label": "Cargament…", + "media_gallery.toggle_visible": "Modificar la visibilitat", + "missing_indicator.label": "Pas trobat", + "navigation_bar.blocks": "Personas blocadas", + "navigation_bar.community_timeline": "Fil public local", + "navigation_bar.edit_profile": "Modificar lo perfil", + "navigation_bar.favourites": "Favorits", + "navigation_bar.follow_requests": "Demandas d'abonament", + "navigation_bar.info": "Mai informacions", + "navigation_bar.logout": "Desconnexion", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Preferéncias", + "navigation_bar.public_timeline": "Fil public global", + "notification.favourite": "{name} a apondut a sos favorits :", + "notification.follow": "{name} vos sèc.", + "notification.reblog": "{name} a partejat vòstre estatut :", + "notifications.clear": "Levar", + "notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?", + "notifications.column_settings.alert": "Notificacions localas", + "notifications.column_settings.favourite": "Favorits :", + "notifications.column_settings.follow": "Nòus abonats :", + "notifications.column_settings.mention": "Mencions :", + "notifications.column_settings.reblog": "Partatges :", + "notifications.column_settings.show": "Mostrar dins la colomna", + "notifications.column_settings.sound": "Emetre un son", + "notifications.settings": "Paramètres de la colomna", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Ajustar la confidencialitat del messatge", + "privacy.direct.long": "Mostrar pas qu'a las personas mencionadas", + "privacy.direct.short": "Dirècte", + "privacy.private.long": "Mostrar pas qu'a vòstres abonats", + "privacy.private.short": "Privat", + "privacy.public.long": "Mostrar dins los fils publics", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Mostrar pas dins los fils publics", + "privacy.unlisted.short": "Pas-listat", + "reply_indicator.cancel": "Anullar", + "report.heading": "Nòu senhalament", + "report.placeholder": "Comentaris addicionals", + "report.submit": "Mandat", + "report.target": "Senhalament", + "search.placeholder": "Recercar", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Escafar", + "status.favourite": "Apondre als favorits", + "status.load_more": "Cargar mai", + "status.media_hidden": "Mèdia rescondut", + "status.mention": "Mencionar", + "status.open": "Desplegar aqueste estatut", + "status.reblog": "Partejar", + "status.reblogged_by": "{name} a partejat :", + "status.reply": "Respondre", + "status.replyAll": "Reply to thread", + "status.report": "Senhalar @{name}", + "status.sensitive_toggle": "Clicar per mostrar", + "status.sensitive_warning": "Contengut embarrassant", + "status.show_less": "Tornar plegar", + "status.show_more": "Desplegar", + "tabs_bar.compose": "Compausar", + "tabs_bar.federated_timeline": "Fil public global", + "tabs_bar.home": "Acuèlh", + "tabs_bar.local_timeline": "Fil public local", + "tabs_bar.notifications": "Notifications", + "upload_area.title": "Lisatz e depausatz per mandar", + "upload_button.label": "Apondre un mèdia", + "upload_form.undo": "Anullar", + "upload_progress.label": "Mandadís…", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Activar/Desactivar lo son", + "video_player.toggle_visible": "Mostrar/Rescondre la vidèo", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json @@ -0,0 +1,163 @@ +{ + "account.block": "Bloquear @{name}", + "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", + "account.edit_profile": "Editar perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.follows": "Segue", + "account.follows_you": "É teu seguidor", + "account.mention": "Mencionar @{name}", + "account.mute": "Silenciar @{name}", + "account.posts": "Posts", + "account.report": "Denunciar @{name}", + "account.requested": "A aguardar aprovação", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Deixar de seguir", + "account.unmute": "Não silenciar @{name}", + "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "column.blocks": "Utilizadores Bloqueados", + "column.community": "Local", + "column.favourites": "Favoritos", + "column.follow_requests": "Seguidores Pendentes", + "column.home": "Home", + "column.mutes": "Utilizadores silenciados", + "column.notifications": "Notificações", + "column.public": "Global", + "column_back_button.label": "Voltar", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Em que estás a pensar?", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", + "compose_form.publish": "Publicar", + "compose_form.sensitive": "Marcar media como conteúdo sensível", + "compose_form.spoiler": "Esconder texto com aviso", + "compose_form.spoiler_placeholder": "Aviso de conteúdo", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Inserir Emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Ainda não existem conteúdo local para mostrar!", + "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", + "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", + "empty_column.home.public_timeline": "global", + "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", + "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rejeitar", + "getting_started.apps": "Existem várias aplicações disponíveis", + "getting_started.heading": "Primeiros passos", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", + "home.column_settings.advanced": "Avançado", + "home.column_settings.basic": "Básico", + "home.column_settings.filter_regex": "Filtrar com uma expressão regular", + "home.column_settings.show_reblogs": "Mostrar as partilhas", + "home.column_settings.show_replies": "Mostrar as respostas", + "home.settings": "Parâmetros da listagem Home", + "lightbox.close": "Fechar", + "loading_indicator.label": "Carregando...", + "media_gallery.toggle_visible": "Esconder/Mostrar", + "missing_indicator.label": "Não encontrado", + "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.community_timeline": "Local", + "navigation_bar.edit_profile": "Editar perfil", + "navigation_bar.favourites": "Favoritos", + "navigation_bar.follow_requests": "Seguidores pendentes", + "navigation_bar.info": "Mais informações", + "navigation_bar.logout": "Sair", + "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Global", + "notification.favourite": "{name} adicionou o teu post aos favoritos", + "notification.follow": "{name} seguiu-te", + "notification.reblog": "{name} partilhou o teu post", + "notifications.clear": "Limpar notificações", + "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.reblog": "Partilhas:", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.sound": "Reproduzir som", + "notifications.settings": "Parâmetros da listagem de Notificações", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Ajustar a privacidade da mensagem", + "privacy.direct.long": "Apenas para utilizadores mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Apenas para os seguidores", + "privacy.private.short": "Privado", + "privacy.public.long": "Publicar em todos os feeds", + "privacy.public.short": "Público", + "privacy.unlisted.long": "Não publicar nos feeds públicos", + "privacy.unlisted.short": "Não listar", + "reply_indicator.cancel": "Cancelar", + "report.heading": "Nova denúncia", + "report.placeholder": "Comentários adicionais", + "report.submit": "Enviar", + "report.target": "Denunciar", + "search.placeholder": "Pesquisar", + "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Eliminar", + "status.favourite": "Adicionar aos favoritos", + "status.load_more": "Carregar mais", + "status.media_hidden": "Media escondida", + "status.mention": "Mencionar @{name}", + "status.open": "Expandir", + "status.reblog": "Partilhar", + "status.reblogged_by": "{name} partilhou", + "status.reply": "Responder", + "status.replyAll": "Reply to thread", + "status.report": "Denúnciar @{name}", + "status.sensitive_toggle": "Clique para ver", + "status.sensitive_warning": "Conteúdo sensível", + "status.show_less": "Mostrar menos", + "status.show_more": "Mostrar mais", + "tabs_bar.compose": "Criar", + "tabs_bar.federated_timeline": "Global", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificações", + "upload_area.title": "Arraste e solte para enviar", + "upload_button.label": "Adicionar media", + "upload_form.undo": "Anular", + "upload_progress.label": "A gravar...", + "video_player.expand": "Expandir vídeo", + "video_player.toggle_sound": "Ligar/Desligar som", + "video_player.toggle_visible": "Ligar/Desligar vídeo", + "video_player.video_error": "Não é possível ver o vídeo" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json @@ -0,0 +1,163 @@ +{ + "account.block": "Bloquear @{name}", + "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", + "account.edit_profile": "Editar perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.follows": "Segue", + "account.follows_you": "É teu seguidor", + "account.mention": "Mencionar @{name}", + "account.mute": "Silenciar @{name}", + "account.posts": "Posts", + "account.report": "Denunciar @{name}", + "account.requested": "A aguardar aprovação", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Deixar de seguir", + "account.unmute": "Não silenciar @{name}", + "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "column.blocks": "Utilizadores Bloqueados", + "column.community": "Local", + "column.favourites": "Favoritos", + "column.follow_requests": "Seguidores Pendentes", + "column.home": "Home", + "column.mutes": "Utilizadores silenciados", + "column.notifications": "Notificações", + "column.public": "Global", + "column_back_button.label": "Voltar", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Em que estás a pensar?", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", + "compose_form.publish": "Publicar", + "compose_form.sensitive": "Marcar media como conteúdo sensível", + "compose_form.spoiler": "Esconder texto com aviso", + "compose_form.spoiler_placeholder": "Aviso de conteúdo", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Inserir Emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "Ainda não existem conteúdo local para mostrar!", + "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", + "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", + "empty_column.home.public_timeline": "global", + "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", + "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rejeitar", + "getting_started.apps": "Existem várias aplicações disponíveis", + "getting_started.heading": "Primeiros passos", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", + "home.column_settings.advanced": "Avançado", + "home.column_settings.basic": "Básico", + "home.column_settings.filter_regex": "Filtrar com uma expressão regular", + "home.column_settings.show_reblogs": "Mostrar as partilhas", + "home.column_settings.show_replies": "Mostrar as respostas", + "home.settings": "Parâmetros da listagem Home", + "lightbox.close": "Fechar", + "loading_indicator.label": "Carregando...", + "media_gallery.toggle_visible": "Esconder/Mostrar", + "missing_indicator.label": "Não encontrado", + "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.community_timeline": "Local", + "navigation_bar.edit_profile": "Editar perfil", + "navigation_bar.favourites": "Favoritos", + "navigation_bar.follow_requests": "Seguidores pendentes", + "navigation_bar.info": "Mais informações", + "navigation_bar.logout": "Sair", + "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Global", + "notification.favourite": "{name} adicionou o teu post aos favoritos", + "notification.follow": "{name} seguiu-te", + "notification.reblog": "{name} partilhou o teu post", + "notifications.clear": "Limpar notificações", + "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.reblog": "Partilhas:", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.sound": "Reproduzir som", + "notifications.settings": "Parâmetros da listagem de Notificações", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Ajustar a privacidade da mensagem", + "privacy.direct.long": "Apenas para utilizadores mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Apenas para os seguidores", + "privacy.private.short": "Privado", + "privacy.public.long": "Publicar em todos os feeds", + "privacy.public.short": "Público", + "privacy.unlisted.long": "Não publicar nos feeds públicos", + "privacy.unlisted.short": "Não listar", + "reply_indicator.cancel": "Cancelar", + "report.heading": "Nova denúncia", + "report.placeholder": "Comentários adicionais", + "report.submit": "Enviar", + "report.target": "Denunciar", + "search.placeholder": "Pesquisar", + "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Eliminar", + "status.favourite": "Adicionar aos favoritos", + "status.load_more": "Carregar mais", + "status.media_hidden": "Media escondida", + "status.mention": "Mencionar @{name}", + "status.open": "Expandir", + "status.reblog": "Partilhar", + "status.reblogged_by": "{name} partilhou", + "status.reply": "Responder", + "status.replyAll": "Reply to thread", + "status.report": "Denúnciar @{name}", + "status.sensitive_toggle": "Clique para ver", + "status.sensitive_warning": "Conteúdo sensível", + "status.show_less": "Mostrar menos", + "status.show_more": "Mostrar mais", + "tabs_bar.compose": "Criar", + "tabs_bar.federated_timeline": "Global", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificações", + "upload_area.title": "Arraste e solte para enviar", + "upload_button.label": "Adicionar media", + "upload_form.undo": "Anular", + "upload_progress.label": "A gravar...", + "video_player.expand": "Expandir vídeo", + "video_player.toggle_sound": "Ligar/Desligar som", + "video_player.toggle_visible": "Ligar/Desligar vídeo", + "video_player.video_error": "Não é possível ver o vídeo" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json @@ -0,0 +1,163 @@ +{ + "account.block": "Блокировать", + "account.disclaimer": "Это пользователь с другого узла. Число может быть больше.", + "account.edit_profile": "Изменить профиль", + "account.follow": "Подписаться", + "account.followers": "Подписаны", + "account.follows": "Подписки", + "account.follows_you": "Подписан(а) на Вас", + "account.mention": "Упомянуть", + "account.mute": "Заглушить", + "account.posts": "Посты", + "account.report": "Пожаловаться", + "account.requested": "Ожидает подтверждения", + "account.unblock": "Разблокировать", + "account.unfollow": "Отписаться", + "account.unmute": "Снять глушение", + "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", + "column.blocks": "Список блокировки", + "column.community": "Локальная лента", + "column.favourites": "Понравившееся", + "column.follow_requests": "Запросы на подписку", + "column.home": "Главная", + "column.mutes": "Список глушения", + "column.notifications": "Уведомления", + "column.public": "Глобальная лента", + "column_back_button.label": "Назад", + "column_subheading.navigation": "Навигация", + "column_subheading.settings": "Настройки", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "О чем Вы думаете?", + "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.", + "compose_form.publish": "Трубить", + "compose_form.sensitive": "Отметить как чувствительный контент", + "compose_form.spoiler": "Скрыть текст за предупреждением", + "compose_form.spoiler_placeholder": "Предупреждение о скрытом тексте", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Занятия", + "emoji_button.flags": "Флаги", + "emoji_button.food": "Еда и напитки", + "emoji_button.label": "Вставить эмодзи", + "emoji_button.nature": "Природа", + "emoji_button.objects": "Предметы", + "emoji_button.people": "Люди", + "emoji_button.search": "Найти...", + "emoji_button.symbols": "Символы", + "emoji_button.travel": "Путешествия", + "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", + "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", + "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", + "empty_column.home.public_timeline": "публичные ленты", + "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", + "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", + "follow_request.authorize": "Авторизовать", + "follow_request.reject": "Отказать", + "getting_started.apps": "Доступны различные приложения.", + "getting_started.heading": "Добро пожаловать", + "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.", + "home.column_settings.advanced": "Дополнительные", + "home.column_settings.basic": "Основные", + "home.column_settings.filter_regex": "Отфильтровать регулярным выражением", + "home.column_settings.show_reblogs": "Показывать продвижения", + "home.column_settings.show_replies": "Показывать ответы", + "home.settings": "Настройки колонки", + "lightbox.close": "Закрыть", + "loading_indicator.label": "Загрузка...", + "media_gallery.toggle_visible": "Показать/скрыть", + "missing_indicator.label": "Не найдено", + "navigation_bar.blocks": "Список блокировки", + "navigation_bar.community_timeline": "Локальная лента", + "navigation_bar.edit_profile": "Изменить профиль", + "navigation_bar.favourites": "Понравившееся", + "navigation_bar.follow_requests": "Запросы на подписку", + "navigation_bar.info": "Об узле", + "navigation_bar.logout": "Выйти", + "navigation_bar.mutes": "Список глушения", + "navigation_bar.preferences": "Опции", + "navigation_bar.public_timeline": "Глобальная лента", + "notification.favourite": "{name} понравился Ваш статус", + "notification.follow": "{name} подписался(-лась) на Вас", + "notification.reblog": "{name} продвинул(а) Ваш статус", + "notifications.clear": "Очистить уведомления", + "notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?", + "notifications.column_settings.alert": "Десктопные уведомления", + "notifications.column_settings.favourite": "Нравится:", + "notifications.column_settings.follow": "Новые подписчики:", + "notifications.column_settings.mention": "Упоминания:", + "notifications.column_settings.reblog": "Продвижения:", + "notifications.column_settings.show": "Показывать в колонке", + "notifications.column_settings.sound": "Проигрывать звук", + "notifications.settings": "Настройки колонки", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Изменить видимость статуса", + "privacy.direct.long": "Показать только упомянутым", + "privacy.direct.short": "Направленный", + "privacy.private.long": "Показать только подписчикам", + "privacy.private.short": "Приватный", + "privacy.public.long": "Показать в публичных лентах", + "privacy.public.short": "Публичный", + "privacy.unlisted.long": "Не показывать в лентах", + "privacy.unlisted.short": "Скрытый", + "reply_indicator.cancel": "Отмена", + "report.heading": "Новая жалоба", + "report.placeholder": "Комментарий", + "report.submit": "Отправить", + "report.target": "Жалуемся на", + "search.placeholder": "Поиск", + "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", + "status.cannot_reblog": "Этот статус не может быть продвинут", + "status.delete": "Удалить", + "status.favourite": "Нравится", + "status.load_more": "Показать еще", + "status.media_hidden": "Медиаконтент скрыт", + "status.mention": "Упомянуть @{name}", + "status.open": "Развернуть статус", + "status.reblog": "Продвинуть", + "status.reblogged_by": "{name} продвинул(а)", + "status.reply": "Ответить", + "status.replyAll": "Ответить на тред", + "status.report": "Пожаловаться", + "status.sensitive_toggle": "Нажмите для просмотра", + "status.sensitive_warning": "Чувствительный контент", + "status.show_less": "Свернуть", + "status.show_more": "Развернуть", + "tabs_bar.compose": "Написать", + "tabs_bar.federated_timeline": "Глобальная", + "tabs_bar.home": "Главная", + "tabs_bar.local_timeline": "Локальная", + "tabs_bar.notifications": "Уведомления", + "upload_area.title": "Перетащите сюда, чтобы загрузить", + "upload_button.label": "Добавить медиаконтент", + "upload_form.undo": "Отменить", + "upload_progress.label": "Загрузка...", + "video_player.expand": "Развернуть видео", + "video_player.toggle_sound": "Вкл./выкл. звук", + "video_player.toggle_visible": "Показать/скрыть", + "video_player.video_error": "Видео не может быть проиграно" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json @@ -0,0 +1,163 @@ +{ + "account.block": "Заблокувати", + "account.disclaimer": "This user is from another instance. This number may be larger.", + "account.edit_profile": "Налаштування профілю", + "account.follow": "Підписатися", + "account.followers": "Підписники", + "account.follows": "Підписки", + "account.follows_you": "Підписаний", + "account.mention": "Згадати", + "account.mute": "Mute @{name}", + "account.posts": "Пости", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.unblock": "Розблокувати", + "account.unfollow": "Відписатися", + "account.unmute": "Unmute @{name}", + "boost_modal.combo": "You can press {combo} to skip this next time", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Головна", + "column.mutes": "Muted users", + "column.notifications": "Сповіщення", + "column.public": "Стіна", + "column_back_button.label": "Назад", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "Що у Вас на думці?", + "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", + "compose_form.publish": "Дмухнути", + "compose_form.sensitive": "Непристойний зміст", + "compose_form.spoiler": "Hide text behind warning", + "compose_form.spoiler_placeholder": "Content warning", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.apps": "Various apps are available", + "getting_started.heading": "Ласкаво просимо", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", + "home.column_settings.advanced": "Advanced", + "home.column_settings.basic": "Basic", + "home.column_settings.filter_regex": "Filter out by regular expressions", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.settings": "Column settings", + "lightbox.close": "Закрити", + "loading_indicator.label": "Завантаження...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.edit_profile": "Редагувати профіль", + "navigation_bar.favourites": "Favourites", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "Extended information", + "navigation_bar.logout": "Вийти", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "Налаштування", + "navigation_bar.public_timeline": "Публічна стіна", + "notification.favourite": "{name} сподобався ваш допис", + "notification.follow": "{name} підписався(-лась) на Вас", + "notification.reblog": "{name} передмухнув(-ла) Ваш статус", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.settings": "Column settings", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Відмінити", + "report.heading": "New report", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting", + "search.placeholder": "Пошук", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "Видалити", + "status.favourite": "Подобається", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Згадати", + "status.open": "Expand this status", + "status.reblog": "Передмухнути", + "status.reblogged_by": "{name} передмухнув(-ла)", + "status.reply": "Відповісти", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Натисніть, щоб подивитися", + "status.sensitive_warning": "Непристойний зміст", + "status.show_less": "Show less", + "status.show_more": "Show more", + "tabs_bar.compose": "Написати", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Головна", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Сповіщення", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Додати медіа", + "upload_form.undo": "Відмінити", + "upload_progress.label": "Uploading...", + "video_player.expand": "Expand video", + "video_player.toggle_sound": "Увімкнути/вимкнути звук", + "video_player.toggle_visible": "Toggle visibility", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_ar.json b/app/javascript/mastodon/locales/whitelist_ar.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_bg.json b/app/javascript/mastodon/locales/whitelist_bg.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_de.json b/app/javascript/mastodon/locales/whitelist_de.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_en.json b/app/javascript/mastodon/locales/whitelist_en.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_eo.json b/app/javascript/mastodon/locales/whitelist_eo.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_es.json b/app/javascript/mastodon/locales/whitelist_es.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_fa.json b/app/javascript/mastodon/locales/whitelist_fa.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_fi.json b/app/javascript/mastodon/locales/whitelist_fi.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_fr.json b/app/javascript/mastodon/locales/whitelist_fr.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_hr.json b/app/javascript/mastodon/locales/whitelist_hr.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_hu.json b/app/javascript/mastodon/locales/whitelist_hu.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_id.json b/app/javascript/mastodon/locales/whitelist_id.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_io.json b/app/javascript/mastodon/locales/whitelist_io.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_it.json b/app/javascript/mastodon/locales/whitelist_it.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_ja.json b/app/javascript/mastodon/locales/whitelist_ja.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_nl.json b/app/javascript/mastodon/locales/whitelist_nl.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_no.json b/app/javascript/mastodon/locales/whitelist_no.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_oc.json b/app/javascript/mastodon/locales/whitelist_oc.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_pt-BR.json b/app/javascript/mastodon/locales/whitelist_pt-BR.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_pt.json b/app/javascript/mastodon/locales/whitelist_pt.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_ru.json b/app/javascript/mastodon/locales/whitelist_ru.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_uk.json b/app/javascript/mastodon/locales/whitelist_uk.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_zh-CN.json b/app/javascript/mastodon/locales/whitelist_zh-CN.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json @@ -0,0 +1,2 @@ +[ +]+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json @@ -0,0 +1,163 @@ +{ + "account.block": "屏蔽 @{name}", + "account.disclaimer": "由于这个账户处于另一个服务站,实际数字会比这个更多。", + "account.edit_profile": "修改个人资料", + "account.follow": "关注", + "account.followers": "关注者", + "account.follows": "正关注", + "account.follows_you": "关注你", + "account.mention": "提及 @{name}", + "account.mute": "将 @{name} 静音", + "account.posts": "嘟文", + "account.report": "举报 @{name}", + "account.requested": "等候审批", + "account.unblock": "解除对 @{name} 的屏蔽", + "account.unfollow": "取消关注", + "account.unmute": "取消 @{name} 的静音", + "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", + "column.blocks": "屏蔽用户", + "column.community": "本站时间轴", + "column.favourites": "赞过的嘟文", + "column.follow_requests": "关注请求", + "column.home": "主页", + "column.mutes": "Muted users", + "column.notifications": "通知", + "column.public": "跨站公共时间轴", + "column_back_button.label": "返回", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "在想啥?", + "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务站} other {之中有些不是 Mastodon 服务站}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。", + "compose_form.publish": "嘟嘟", + "compose_form.sensitive": "将媒体文件标示为“敏感内容”", + "compose_form.spoiler": "将部分文本藏于警告消息之后", + "compose_form.spoiler_placeholder": "敏感内容的警告消息", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "加入表情符号", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!", + "empty_column.hashtag": "这个标签暂时未有内容。", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.home.public_timeline": "公共时间轴", + "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", + "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务站的用户吧!你和本站、友站的交流,将决定这里出现的内容。", + "follow_request.authorize": "批准", + "follow_request.reject": "拒绝", + "getting_started.apps": "手机或桌面应用程序", + "getting_started.heading": "开始使用", + "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。你亦可通过{apps}阅读 Mastodon 上的消息。", + "home.column_settings.advanced": "高端", + "home.column_settings.basic": "基本", + "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤", + "home.column_settings.show_reblogs": "显示被转的嘟文", + "home.column_settings.show_replies": "显示回应嘟文", + "home.settings": "字段设置", + "lightbox.close": "关闭", + "loading_indicator.label": "加载中……", + "media_gallery.toggle_visible": "打开或关上", + "missing_indicator.label": "找不到内容", + "navigation_bar.blocks": "被屏蔽的用户", + "navigation_bar.community_timeline": "本站时间轴", + "navigation_bar.edit_profile": "修改个人资料", + "navigation_bar.favourites": "赞的内容", + "navigation_bar.follow_requests": "关注请求", + "navigation_bar.info": "关于本服务站", + "navigation_bar.logout": "注销", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "首选项", + "navigation_bar.public_timeline": "跨站公共时间轴", + "notification.favourite": "{name} 赞你的嘟文", + "notification.follow": "{name} 开始关注你", + "notification.reblog": "{name} 转嘟你的嘟文", + "notifications.clear": "清空通知纪录", + "notifications.clear_confirmation": "你确定要清空通知纪录吗?", + "notifications.column_settings.alert": "显示桌面通知", + "notifications.column_settings.favourite": "赞你的嘟文:", + "notifications.column_settings.follow": "关注你:", + "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.reblog": "转你的嘟文:", + "notifications.column_settings.show": "在通知栏显示", + "notifications.column_settings.sound": "播放音效", + "notifications.settings": "字段设置", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "调整隐私设置", + "privacy.direct.long": "只有提及的用户能看到", + "privacy.direct.short": "私人消息", + "privacy.private.long": "只有关注你用户能看到", + "privacy.private.short": "关注者", + "privacy.public.long": "在公共时间轴显示", + "privacy.public.short": "公共", + "privacy.unlisted.long": "公开,但不在公共时间轴显示", + "privacy.unlisted.short": "公开", + "reply_indicator.cancel": "取消", + "report.heading": "举报", + "report.placeholder": "额外消息", + "report.submit": "提交", + "report.target": "Reporting", + "search.placeholder": "搜索", + "search_results.total": "{count, number} 项结果", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "删除", + "status.favourite": "赞", + "status.load_more": "加载更多", + "status.media_hidden": "隐藏媒体内容", + "status.mention": "提及 @{name}", + "status.open": "展开嘟文", + "status.reblog": "转嘟", + "status.reblogged_by": "{name} 转嘟", + "status.reply": "回应", + "status.replyAll": "Reply to thread", + "status.report": "举报 @{name}", + "status.sensitive_toggle": "点击显示", + "status.sensitive_warning": "敏感内容", + "status.show_less": "减少显示", + "status.show_more": "显示更多", + "tabs_bar.compose": "撰写", + "tabs_bar.federated_timeline": "跨站", + "tabs_bar.home": "主页", + "tabs_bar.local_timeline": "本站", + "tabs_bar.notifications": "通知", + "upload_area.title": "将文件拖放至此上传", + "upload_button.label": "上传媒体文件", + "upload_form.undo": "还原", + "upload_progress.label": "上传中……", + "video_player.expand": "展开影片", + "video_player.toggle_sound": "开关音效", + "video_player.toggle_visible": "打开或关上", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json @@ -0,0 +1,163 @@ +{ + "account.block": "封鎖 @{name}", + "account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。", + "account.edit_profile": "修改個人資料", + "account.follow": "關注", + "account.followers": "關注的人", + "account.follows": "正在關注", + "account.follows_you": "關注你", + "account.mention": "提及 @{name}", + "account.mute": "將 @{name} 靜音", + "account.posts": "文章", + "account.report": "舉報 @{name}", + "account.requested": "等候審批", + "account.unblock": "解除對 @{name} 的封鎖", + "account.unfollow": "取消關注", + "account.unmute": "取消 @{name} 的靜音", + "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", + "column.blocks": "封鎖用戶", + "column.community": "本站時間軸", + "column.favourites": "喜歡的文章", + "column.follow_requests": "關注請求", + "column.home": "主頁", + "column.mutes": "Muted users", + "column.notifications": "通知", + "column.public": "跨站公共時間軸", + "column_back_button.label": "返回", + "column_subheading.navigation": "Navigation", + "column_subheading.settings": "Settings", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "你在想甚麼?", + "compose_form.privacy_disclaimer": "你的私人文章,將被遞送至你所提及的 {domains} 用戶。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於各 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將無法收到這篇文章的私隱設定,然後可能被轉推給不能預知的用戶閱讀。", + "compose_form.publish": "發文", + "compose_form.sensitive": "將媒體檔案標示為「敏感內容」", + "compose_form.spoiler": "將部份文字藏於警告訊息之後", + "compose_form.spoiler_placeholder": "敏感警告訊息", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "emoji_button.activity": "Activity", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "加入表情符號", + "emoji_button.nature": "Nature", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.search": "Search...", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!", + "empty_column.hashtag": "這個標籤暫時未有內容。", + "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", + "empty_column.home.public_timeline": "公共時間軸", + "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。", + "empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。", + "follow_request.authorize": "批准", + "follow_request.reject": "拒絕", + "getting_started.apps": "手機或桌面應用程式", + "getting_started.heading": "開始使用", + "getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。", + "home.column_settings.advanced": "進階", + "home.column_settings.basic": "基本", + "home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾", + "home.column_settings.show_reblogs": "顯示被轉推的文章", + "home.column_settings.show_replies": "顯示回應文章", + "home.settings": "欄位設定", + "lightbox.close": "Close", + "loading_indicator.label": "載入中...", + "media_gallery.toggle_visible": "打開或關上", + "missing_indicator.label": "找不到內容", + "navigation_bar.blocks": "被封鎖的用戶", + "navigation_bar.community_timeline": "本站時間軸", + "navigation_bar.edit_profile": "修改個人資料", + "navigation_bar.favourites": "喜歡的內容", + "navigation_bar.follow_requests": "關注請求", + "navigation_bar.info": "關於本服務站", + "navigation_bar.logout": "登出", + "navigation_bar.mutes": "Muted users", + "navigation_bar.preferences": "偏好設定", + "navigation_bar.public_timeline": "跨站公共時間軸", + "notification.favourite": "{name} 喜歡你的文章", + "notification.follow": "{name} 開始關注你", + "notification.reblog": "{name} 轉推你的文章", + "notifications.clear": "清空通知紀錄", + "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?", + "notifications.column_settings.alert": "顯示桌面通知", + "notifications.column_settings.favourite": "喜歡你的文章:", + "notifications.column_settings.follow": "關注你:", + "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.reblog": "轉推你的文章:", + "notifications.column_settings.show": "在通知欄顯示", + "notifications.column_settings.sound": "播放音效", + "notifications.settings": "欄位設定", + "onboarding.done": "Done", + "onboarding.next": "Next", + "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", + "onboarding.page_four.home": "The home timeline shows posts from people you follow.", + "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", + "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.welcome": "Welcome to Mastodon!", + "onboarding.page_six.admin": "Your instance's admin is {admin}.", + "onboarding.page_six.almost_done": "Almost done...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "community guidelines", + "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", + "onboarding.page_six.various_app": "mobile apps", + "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", + "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", + "onboarding.skip": "Skip", + "privacy.change": "調整私隱設定", + "privacy.direct.long": "只有提及的用戶能看到", + "privacy.direct.short": "私人訊息", + "privacy.private.long": "只有關注你用戶能看到", + "privacy.private.short": "關注者", + "privacy.public.long": "在公共時間軸顯示", + "privacy.public.short": "公共", + "privacy.unlisted.long": "公開,但不在公共時間軸顯示", + "privacy.unlisted.short": "公開", + "reply_indicator.cancel": "取消", + "report.heading": "舉報", + "report.placeholder": "額外訊息", + "report.submit": "提交", + "report.target": "Reporting", + "search.placeholder": "搜尋", + "search_results.total": "{count, number} 項結果", + "status.cannot_reblog": "This post cannot be boosted", + "status.delete": "刪除", + "status.favourite": "喜歡", + "status.load_more": "載入更多", + "status.media_hidden": "隱藏媒體內容", + "status.mention": "提及 @{name}", + "status.open": "展開文章", + "status.reblog": "轉推", + "status.reblogged_by": "{name} 轉推", + "status.reply": "回應", + "status.replyAll": "Reply to thread", + "status.report": "舉報 @{name}", + "status.sensitive_toggle": "點擊顯示", + "status.sensitive_warning": "敏感內容", + "status.show_less": "減少顯示", + "status.show_more": "顯示更多", + "tabs_bar.compose": "撰寫", + "tabs_bar.federated_timeline": "跨站", + "tabs_bar.home": "主頁", + "tabs_bar.local_timeline": "本站", + "tabs_bar.notifications": "通知", + "upload_area.title": "將檔案拖放至此上載", + "upload_button.label": "上載媒體檔案", + "upload_form.undo": "還原", + "upload_progress.label": "上載中……", + "video_player.expand": "展開影片", + "video_player.toggle_sound": "開關音效", + "video_player.toggle_visible": "打開或關上", + "video_player.video_error": "Video could not be played" +}+ \ No newline at end of file diff --git a/app/javascript/mastodon/middleware/errors.js b/app/javascript/mastodon/middleware/errors.js @@ -0,0 +1,33 @@ +import { showAlert } from '../actions/alerts'; + +const defaultSuccessSuffix = 'SUCCESS'; +const defaultFailSuffix = 'FAIL'; + +export default function errorsMiddleware() { + return ({ dispatch }) => next => action => { + if (action.type && !action.skipAlert) { + const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); + const isSuccess = new RegExp(`${defaultSuccessSuffix}$`, 'g'); + + if (action.type.match(isFail)) { + if (action.error.response) { + const { data, status, statusText } = action.error.response; + + let message = statusText; + let title = `${status}`; + + if (data.error) { + message = data.error; + } + + dispatch(showAlert(title, message)); + } else { + console.error(action.error); // eslint-disable-line no-console + dispatch(showAlert('Oops!', 'An unexpected error occurred.')); + } + } + } + + return next(action); + }; +}; diff --git a/app/javascript/mastodon/middleware/loading_bar.js b/app/javascript/mastodon/middleware/loading_bar.js @@ -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/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/middleware/sounds.js @@ -0,0 +1,22 @@ +const play = audio => { + if (!audio.paused) { + audio.pause(); + audio.fastSeek(0); + } + + audio.play(); +}; + +export default function soundsMiddleware() { + const soundCache = { + boop: new Audio(['/sounds/boop.mp3']) + }; + + return ({ dispatch }) => next => (action) => { + if (action.meta && action.meta.sound && soundCache[action.meta.sound]) { + play(soundCache[action.meta.sound]); + } + + return next(action); + }; +}; diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js @@ -0,0 +1,133 @@ +import { + ACCOUNT_FETCH_SUCCESS, + FOLLOWERS_FETCH_SUCCESS, + FOLLOWERS_EXPAND_SUCCESS, + FOLLOWING_FETCH_SUCCESS, + FOLLOWING_EXPAND_SUCCESS, + ACCOUNT_TIMELINE_FETCH_SUCCESS, + ACCOUNT_TIMELINE_EXPAND_SUCCESS, + FOLLOW_REQUESTS_FETCH_SUCCESS, + FOLLOW_REQUESTS_EXPAND_SUCCESS +} from '../actions/accounts'; +import { + BLOCKS_FETCH_SUCCESS, + BLOCKS_EXPAND_SUCCESS +} from '../actions/blocks'; +import { + MUTES_FETCH_SUCCESS, + MUTES_EXPAND_SUCCESS +} from '../actions/mutes'; +import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; +import { + REBLOG_SUCCESS, + UNREBLOG_SUCCESS, + FAVOURITE_SUCCESS, + UNFAVOURITE_SUCCESS, + REBLOGS_FETCH_SUCCESS, + FAVOURITES_FETCH_SUCCESS +} from '../actions/interactions'; +import { + TIMELINE_REFRESH_SUCCESS, + TIMELINE_UPDATE, + TIMELINE_EXPAND_SUCCESS +} from '../actions/timelines'; +import { + STATUS_FETCH_SUCCESS, + CONTEXT_FETCH_SUCCESS +} from '../actions/statuses'; +import { SEARCH_FETCH_SUCCESS } from '../actions/search'; +import { + NOTIFICATIONS_UPDATE, + 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'; + +const normalizeAccount = (state, account) => { + account = { ...account }; + + delete account.followers_count; + delete account.following_count; + delete account.statuses_count; + + return state.set(account.id, Immutable.fromJS(account)) +}; + +const normalizeAccounts = (state, accounts) => { + accounts.forEach(account => { + state = normalizeAccount(state, account); + }); + + return state; +}; + +const normalizeAccountFromStatus = (state, status) => { + state = normalizeAccount(state, status.account); + + if (status.reblog && status.reblog.account) { + state = normalizeAccount(state, status.reblog.account); + } + + return state; +}; + +const normalizeAccountsFromStatuses = (state, statuses) => { + statuses.forEach(status => { + state = normalizeAccountFromStatus(state, status); + }); + + return state; +}; + +const initialState = Immutable.Map(); + +export default function accounts(state = initialState, action) { + switch(action.type) { + 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 FOLLOW_REQUESTS_FETCH_SUCCESS: + case FOLLOW_REQUESTS_EXPAND_SUCCESS: + case BLOCKS_FETCH_SUCCESS: + case BLOCKS_EXPAND_SUCCESS: + case MUTES_FETCH_SUCCESS: + case MUTES_EXPAND_SUCCESS: + return normalizeAccounts(state, action.accounts); + case NOTIFICATIONS_REFRESH_SUCCESS: + case NOTIFICATIONS_EXPAND_SUCCESS: + case SEARCH_FETCH_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: + case FAVOURITED_STATUSES_FETCH_SUCCESS: + case FAVOURITED_STATUSES_EXPAND_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/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js @@ -0,0 +1,135 @@ +import { + ACCOUNT_FETCH_SUCCESS, + FOLLOWERS_FETCH_SUCCESS, + FOLLOWERS_EXPAND_SUCCESS, + FOLLOWING_FETCH_SUCCESS, + FOLLOWING_EXPAND_SUCCESS, + ACCOUNT_TIMELINE_FETCH_SUCCESS, + ACCOUNT_TIMELINE_EXPAND_SUCCESS, + FOLLOW_REQUESTS_FETCH_SUCCESS, + FOLLOW_REQUESTS_EXPAND_SUCCESS, + ACCOUNT_FOLLOW_SUCCESS, + ACCOUNT_UNFOLLOW_SUCCESS +} from '../actions/accounts'; +import { + BLOCKS_FETCH_SUCCESS, + BLOCKS_EXPAND_SUCCESS +} from '../actions/blocks'; +import { + MUTES_FETCH_SUCCESS, + MUTES_EXPAND_SUCCESS +} from '../actions/mutes'; +import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; +import { + REBLOG_SUCCESS, + UNREBLOG_SUCCESS, + FAVOURITE_SUCCESS, + UNFAVOURITE_SUCCESS, + REBLOGS_FETCH_SUCCESS, + FAVOURITES_FETCH_SUCCESS +} from '../actions/interactions'; +import { + TIMELINE_REFRESH_SUCCESS, + TIMELINE_UPDATE, + TIMELINE_EXPAND_SUCCESS +} from '../actions/timelines'; +import { + STATUS_FETCH_SUCCESS, + CONTEXT_FETCH_SUCCESS +} from '../actions/statuses'; +import { SEARCH_FETCH_SUCCESS } from '../actions/search'; +import { + NOTIFICATIONS_UPDATE, + 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'; + +const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS({ + followers_count: account.followers_count, + following_count: account.following_count, + statuses_count: account.statuses_count, +})); + +const normalizeAccounts = (state, accounts) => { + accounts.forEach(account => { + state = normalizeAccount(state, account); + }); + + return state; +}; + +const normalizeAccountFromStatus = (state, status) => { + state = normalizeAccount(state, status.account); + + if (status.reblog && status.reblog.account) { + state = normalizeAccount(state, status.reblog.account); + } + + return state; +}; + +const normalizeAccountsFromStatuses = (state, statuses) => { + statuses.forEach(status => { + state = normalizeAccountFromStatus(state, status); + }); + + return state; +}; + +const initialState = Immutable.Map(); + +export default function accountsCounters(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: + return state.merge(action.state.get('accounts_counters')); + 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 FOLLOW_REQUESTS_FETCH_SUCCESS: + case FOLLOW_REQUESTS_EXPAND_SUCCESS: + case BLOCKS_FETCH_SUCCESS: + case BLOCKS_EXPAND_SUCCESS: + case MUTES_FETCH_SUCCESS: + case MUTES_EXPAND_SUCCESS: + return normalizeAccounts(state, action.accounts); + case NOTIFICATIONS_REFRESH_SUCCESS: + case NOTIFICATIONS_EXPAND_SUCCESS: + case SEARCH_FETCH_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: + case FAVOURITED_STATUSES_FETCH_SUCCESS: + case FAVOURITED_STATUSES_EXPAND_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); + case ACCOUNT_FOLLOW_SUCCESS: + return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); + case ACCOUNT_UNFOLLOW_SUCCESS: + return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js @@ -0,0 +1,25 @@ +import { + ALERT_SHOW, + ALERT_DISMISS, + ALERT_CLEAR +} from '../actions/alerts'; +import Immutable from 'immutable'; + +const initialState = Immutable.List([]); + +export default function alerts(state = initialState, action) { + switch(action.type) { + case ALERT_SHOW: + return state.push(Immutable.Map({ + key: state.size > 0 ? state.last().get('key') + 1 : 0, + title: action.title, + message: action.message + })); + case ALERT_DISMISS: + return state.filterNot(item => item.get('key') === action.alert.key); + case ALERT_CLEAR: + return state.clear(); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/cards.js b/app/javascript/mastodon/reducers/cards.js @@ -0,0 +1,14 @@ +import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; + +import Immutable from 'immutable'; + +const initialState = Immutable.Map(); + +export default function cards(state = initialState, action) { + switch(action.type) { + case STATUS_CARD_FETCH_SUCCESS: + return state.set(action.id, Immutable.fromJS(action.card)); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js @@ -0,0 +1,232 @@ +import { + COMPOSE_MOUNT, + COMPOSE_UNMOUNT, + COMPOSE_CHANGE, + COMPOSE_REPLY, + COMPOSE_REPLY_CANCEL, + COMPOSE_MENTION, + COMPOSE_SUBMIT_REQUEST, + COMPOSE_SUBMIT_SUCCESS, + COMPOSE_SUBMIT_FAIL, + COMPOSE_UPLOAD_REQUEST, + COMPOSE_UPLOAD_SUCCESS, + COMPOSE_UPLOAD_FAIL, + COMPOSE_UPLOAD_UNDO, + COMPOSE_UPLOAD_PROGRESS, + COMPOSE_SUGGESTIONS_CLEAR, + COMPOSE_SUGGESTIONS_READY, + COMPOSE_SUGGESTION_SELECT, + COMPOSE_SENSITIVITY_CHANGE, + COMPOSE_SPOILERNESS_CHANGE, + COMPOSE_SPOILER_TEXT_CHANGE, + COMPOSE_VISIBILITY_CHANGE, + COMPOSE_LISTABILITY_CHANGE, + COMPOSE_EMOJI_INSERT +} from '../actions/compose'; +import { TIMELINE_DELETE } from '../actions/timelines'; +import { STORE_HYDRATE } from '../actions/store'; +import Immutable from 'immutable'; +import uuid from '../uuid'; + +const initialState = Immutable.Map({ + mounted: false, + sensitive: false, + spoiler: false, + spoiler_text: '', + privacy: null, + text: '', + focusDate: null, + preselectDate: null, + in_reply_to: null, + is_submitting: false, + is_uploading: false, + progress: 0, + media_attachments: Immutable.List(), + suggestion_token: null, + suggestions: Immutable.List(), + me: null, + default_privacy: 'public', + resetFileKey: Math.floor((Math.random() * 0x10000)), + idempotencyKey: null +}); + +function statusToTextMentions(state, status) { + let set = Immutable.OrderedSet([]); + let me = state.get('me'); + + if (status.getIn(['account', 'id']) !== me) { + set = set.add(`@${status.getIn(['account', 'acct'])} `); + } + + return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); +}; + +function clearAll(state) { + return state.withMutations(map => { + map.set('text', ''); + map.set('spoiler', false); + map.set('spoiler_text', ''); + map.set('is_submitting', false); + map.set('in_reply_to', null); + map.set('privacy', state.get('default_privacy')); + map.set('sensitive', false); + map.update('media_attachments', list => list.clear()); + map.set('idempotencyKey', uuid()); + }); +}; + +function appendMedia(state, media) { + return state.withMutations(map => { + map.update('media_attachments', list => list.push(media)); + map.set('is_uploading', false); + map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); + map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); + map.set('focusDate', new Date()); + map.set('idempotencyKey', uuid()); + }); +}; + +function removeMedia(state, mediaId) { + const media = state.get('media_attachments').find(item => item.get('id') === mediaId); + const prevSize = state.get('media_attachments').size; + + return state.withMutations(map => { + map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId)); + map.update('text', text => text.replace(media.get('text_url'), '').trim()); + map.set('idempotencyKey', uuid()); + + if (prevSize === 1) { + map.set('sensitive', false); + } + }); +}; + +const insertSuggestion = (state, position, token, completion) => { + return state.withMutations(map => { + map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); + map.set('suggestion_token', null); + map.update('suggestions', Immutable.List(), list => list.clear()); + map.set('focusDate', new Date()); + map.set('idempotencyKey', uuid()); + }); +}; + +const insertEmoji = (state, position, emojiData) => { + const emoji = emojiData.shortname; + + return state.withMutations(map => { + map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`); + map.set('focusDate', new Date()); + map.set('idempotencyKey', uuid()); + }); +}; + +const privacyPreference = (a, b) => { + if (a === 'direct' || b === 'direct') { + return 'direct'; + } else if (a === 'private' || b === 'private') { + return 'private'; + } else if (a === 'unlisted' || b === 'unlisted') { + return 'unlisted'; + } else { + return 'public'; + } +}; + +export default function compose(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: + return clearAll(state.merge(action.state.get('compose'))); + case COMPOSE_MOUNT: + return state.set('mounted', true); + case COMPOSE_UNMOUNT: + return state.set('mounted', false); + case COMPOSE_SENSITIVITY_CHANGE: + return state + .set('sensitive', !state.get('sensitive')) + .set('idempotencyKey', uuid()); + case COMPOSE_SPOILERNESS_CHANGE: + return state.withMutations(map => { + map.set('spoiler_text', ''); + map.set('spoiler', !state.get('spoiler')); + map.set('idempotencyKey', uuid()); + }); + case COMPOSE_SPOILER_TEXT_CHANGE: + return state + .set('spoiler_text', action.text) + .set('idempotencyKey', uuid()); + case COMPOSE_VISIBILITY_CHANGE: + return state + .set('privacy', action.value) + .set('idempotencyKey', uuid()); + case COMPOSE_CHANGE: + return state + .set('text', action.text) + .set('idempotencyKey', uuid()); + case COMPOSE_REPLY: + return state.withMutations(map => { + map.set('in_reply_to', action.status.get('id')); + map.set('text', statusToTextMentions(state, action.status)); + map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.set('focusDate', new Date()); + map.set('preselectDate', new Date()); + map.set('idempotencyKey', uuid()); + + if (action.status.get('spoiler_text').length > 0) { + map.set('spoiler', true); + map.set('spoiler_text', action.status.get('spoiler_text')); + } else { + map.set('spoiler', false); + map.set('spoiler_text', ''); + } + }); + case COMPOSE_REPLY_CANCEL: + return state.withMutations(map => { + map.set('in_reply_to', null); + map.set('text', ''); + map.set('spoiler', false); + map.set('spoiler_text', ''); + map.set('privacy', state.get('default_privacy')); + map.set('idempotencyKey', uuid()); + }); + case COMPOSE_SUBMIT_REQUEST: + return state.set('is_submitting', true); + case COMPOSE_SUBMIT_SUCCESS: + return clearAll(state); + case COMPOSE_SUBMIT_FAIL: + return state.set('is_submitting', false); + case COMPOSE_UPLOAD_REQUEST: + return state.withMutations(map => { + map.set('is_uploading', true); + }); + case COMPOSE_UPLOAD_SUCCESS: + return appendMedia(state, Immutable.fromJS(action.media)); + case COMPOSE_UPLOAD_FAIL: + return state.set('is_uploading', false); + case COMPOSE_UPLOAD_UNDO: + return removeMedia(state, action.media_id); + case COMPOSE_UPLOAD_PROGRESS: + return state.set('progress', Math.round((action.loaded / action.total) * 100)); + case COMPOSE_MENTION: + return state + .update('text', text => `${text}@${action.account.get('acct')} `) + .set('focusDate', new Date()) + .set('idempotencyKey', uuid()); + case COMPOSE_SUGGESTIONS_CLEAR: + return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); + case COMPOSE_SUGGESTIONS_READY: + return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); + case COMPOSE_SUGGESTION_SELECT: + return insertSuggestion(state, action.position, action.token, action.completion); + case TIMELINE_DELETE: + if (action.id === state.get('in_reply_to')) { + return state.set('in_reply_to', null); + } else { + return state; + } + case COMPOSE_EMOJI_INSERT: + return insertEmoji(state, action.position, action.emoji); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js @@ -0,0 +1,38 @@ +import { combineReducers } from 'redux-immutable'; +import timelines from './timelines'; +import meta from './meta'; +import compose from './compose'; +import alerts from './alerts'; +import { loadingBarReducer } from 'react-redux-loading-bar'; +import modal from './modal'; +import user_lists from './user_lists'; +import accounts from './accounts'; +import accounts_counters from './accounts_counters'; +import statuses from './statuses'; +import relationships from './relationships'; +import search from './search'; +import notifications from './notifications'; +import settings from './settings'; +import status_lists from './status_lists'; +import cards from './cards'; +import reports from './reports'; + +export default combineReducers({ + timelines, + meta, + compose, + alerts, + loadingBar: loadingBarReducer, + modal, + user_lists, + status_lists, + accounts, + accounts_counters, + statuses, + relationships, + search, + notifications, + settings, + cards, + reports +}); diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js @@ -0,0 +1,17 @@ +import { STORE_HYDRATE } from '../actions/store'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + streaming_api_base_url: null, + access_token: null, + me: null +}); + +export default function meta(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: + return state.merge(action.state.get('meta')); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js @@ -0,0 +1,18 @@ +import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; +import Immutable from 'immutable'; + +const initialState = { + modalType: null, + modalProps: {} +}; + +export default function modal(state = initialState, action) { + switch(action.type) { + case MODAL_OPEN: + return { modalType: action.modalType, modalProps: action.modalProps }; + case MODAL_CLOSE: + return initialState; + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js @@ -0,0 +1,104 @@ +import { + NOTIFICATIONS_UPDATE, + NOTIFICATIONS_REFRESH_SUCCESS, + NOTIFICATIONS_EXPAND_SUCCESS, + NOTIFICATIONS_REFRESH_REQUEST, + NOTIFICATIONS_EXPAND_REQUEST, + NOTIFICATIONS_REFRESH_FAIL, + NOTIFICATIONS_EXPAND_FAIL, + NOTIFICATIONS_CLEAR, + NOTIFICATIONS_SCROLL_TOP +} from '../actions/notifications'; +import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + items: Immutable.List(), + next: null, + top: true, + unread: 0, + loaded: false, + isLoading: true +}); + +const notificationToMap = notification => Immutable.Map({ + id: notification.id, + type: notification.type, + account: notification.account.id, + status: notification.status ? notification.status.id : null +}); + +const normalizeNotification = (state, notification) => { + if (!state.get('top')) { + state = state.update('unread', unread => unread + 1); + } + + return state.update('items', list => list.unshift(notificationToMap(notification))); +}; + +const normalizeNotifications = (state, notifications, next) => { + let items = Immutable.List(); + const loaded = state.get('loaded'); + + notifications.forEach((n, i) => { + items = items.set(i, notificationToMap(n)); + }); + + if (state.get('next') === null) { + state = state.set('next', next); + } + + return state + .update('items', list => loaded ? items.concat(list) : list.concat(items)) + .set('loaded', true) + .set('isLoading', false); +}; + +const appendNormalizedNotifications = (state, notifications, next) => { + let items = Immutable.List(); + + notifications.forEach((n, i) => { + items = items.set(i, notificationToMap(n)); + }); + + return state + .update('items', list => list.concat(items)) + .set('next', next) + .set('isLoading', false); +}; + +const filterNotifications = (state, relationship) => { + return state.update('items', list => list.filterNot(item => item.get('account') === relationship.id)); +}; + +const updateTop = (state, top) => { + if (top) { + state = state.set('unread', 0); + } + + return state.set('top', top); +}; + +export default function notifications(state = initialState, action) { + switch(action.type) { + case NOTIFICATIONS_REFRESH_REQUEST: + case NOTIFICATIONS_EXPAND_REQUEST: + case NOTIFICATIONS_REFRESH_FAIL: + case NOTIFICATIONS_EXPAND_FAIL: + return state.set('isLoading', true); + case NOTIFICATIONS_SCROLL_TOP: + return updateTop(state, action.top); + case NOTIFICATIONS_UPDATE: + return normalizeNotification(state, action.notification); + case NOTIFICATIONS_REFRESH_SUCCESS: + return normalizeNotifications(state, action.notifications, action.next); + case NOTIFICATIONS_EXPAND_SUCCESS: + return appendNormalizedNotifications(state, action.notifications, action.next); + case ACCOUNT_BLOCK_SUCCESS: + return filterNotifications(state, action.relationship); + case NOTIFICATIONS_CLEAR: + return state.set('items', Immutable.List()).set('next', null); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js @@ -0,0 +1,38 @@ +import { + ACCOUNT_FOLLOW_SUCCESS, + ACCOUNT_UNFOLLOW_SUCCESS, + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_UNBLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, + ACCOUNT_UNMUTE_SUCCESS, + RELATIONSHIPS_FETCH_SUCCESS +} from '../actions/accounts'; +import Immutable from 'immutable'; + +const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship)); + +const normalizeRelationships = (state, relationships) => { + relationships.forEach(relationship => { + state = normalizeRelationship(state, relationship); + }); + + return state; +}; + +const initialState = Immutable.Map(); + +export default function relationships(state = initialState, action) { + switch(action.type) { + case ACCOUNT_FOLLOW_SUCCESS: + case ACCOUNT_UNFOLLOW_SUCCESS: + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_UNBLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + case ACCOUNT_UNMUTE_SUCCESS: + return normalizeRelationship(state, action.relationship); + case RELATIONSHIPS_FETCH_SUCCESS: + return normalizeRelationships(state, action.relationships); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js @@ -0,0 +1,60 @@ +import { + REPORT_INIT, + REPORT_SUBMIT_REQUEST, + REPORT_SUBMIT_SUCCESS, + REPORT_SUBMIT_FAIL, + REPORT_CANCEL, + REPORT_STATUS_TOGGLE, + REPORT_COMMENT_CHANGE +} from '../actions/reports'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + new: Immutable.Map({ + isSubmitting: false, + account_id: null, + status_ids: Immutable.Set(), + comment: '' + }) +}); + +export default function reports(state = initialState, action) { + switch(action.type) { + case REPORT_INIT: + return state.withMutations(map => { + map.setIn(['new', 'isSubmitting'], false); + map.setIn(['new', 'account_id'], action.account.get('id')); + + if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { + map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set()); + map.setIn(['new', 'comment'], ''); + } else { + map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); + } + }); + case REPORT_STATUS_TOGGLE: + return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => { + if (action.checked) { + return set.add(action.statusId); + } + + return set.remove(action.statusId); + }); + case REPORT_COMMENT_CHANGE: + return state.setIn(['new', 'comment'], action.comment); + case REPORT_SUBMIT_REQUEST: + return state.setIn(['new', 'isSubmitting'], true); + case REPORT_SUBMIT_FAIL: + return state.setIn(['new', 'isSubmitting'], false); + case REPORT_CANCEL: + case REPORT_SUBMIT_SUCCESS: + return state.withMutations(map => { + map.setIn(['new', 'account_id'], null); + map.setIn(['new', 'status_ids'], Immutable.Set()); + map.setIn(['new', 'comment'], ''); + map.setIn(['new', 'isSubmitting'], false); + }); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js @@ -0,0 +1,96 @@ +import { + SEARCH_CHANGE, + SEARCH_CLEAR, + SEARCH_FETCH_SUCCESS, + SEARCH_SHOW +} from '../actions/search'; +import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + value: '', + submitted: false, + hidden: false, + results: Immutable.Map() +}); + +const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => { + let newSuggestions = []; + + if (accounts.length > 0) { + newSuggestions.push({ + title: 'account', + items: accounts.map(item => ({ + type: 'account', + id: item.id, + value: item.acct + })) + }); + } + + if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 || hashtags.length > 0) { + let hashtagItems = hashtags.map(item => ({ + type: 'hashtag', + id: item, + value: `#${item}` + })); + + if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 && !value.startsWith('http://') && !value.startsWith('https://') && hashtags.indexOf(value) === -1) { + hashtagItems.unshift({ + type: 'hashtag', + id: value, + value: `#${value}` + }); + } + + if (hashtagItems.length > 0) { + newSuggestions.push({ + title: 'hashtag', + items: hashtagItems + }); + } + } + + if (statuses.length > 0) { + newSuggestions.push({ + title: 'status', + items: statuses.map(item => ({ + type: 'status', + id: item.id, + value: item.id + })) + }); + } + + return state.withMutations(map => { + map.set('suggestions', newSuggestions); + map.set('loaded_value', value); + }); +}; + +export default function search(state = initialState, action) { + switch(action.type) { + case SEARCH_CHANGE: + return state.set('value', action.value); + case SEARCH_CLEAR: + return state.withMutations(map => { + map.set('value', ''); + map.set('results', Immutable.Map()); + map.set('submitted', false); + map.set('hidden', false); + }); + case SEARCH_SHOW: + return state.set('hidden', false); + case COMPOSE_REPLY: + case COMPOSE_MENTION: + return state.set('hidden', true); + case SEARCH_FETCH_SUCCESS: + return state.set('results', Immutable.Map({ + accounts: Immutable.List(action.results.accounts.map(item => item.id)), + statuses: Immutable.List(action.results.statuses.map(item => item.id)), + hashtags: Immutable.List(action.results.hashtags) + })).set('submitted', true); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js @@ -0,0 +1,52 @@ +import { SETTING_CHANGE } from '../actions/settings'; +import { STORE_HYDRATE } from '../actions/store'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + onboarded: false, + + home: Immutable.Map({ + shows: Immutable.Map({ + reblog: true, + reply: true + }), + + regex: Immutable.Map({ + body: '' + }) + }), + + notifications: Immutable.Map({ + alerts: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true + }), + + shows: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true + }), + + sounds: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true + }) + }) +}); + +export default function settings(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: + return state.mergeDeep(action.state.get('settings')); + case SETTING_CHANGE: + return state.setIn(action.key, action.value); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js @@ -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').concat(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/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js @@ -0,0 +1,124 @@ +import { + REBLOG_REQUEST, + REBLOG_SUCCESS, + REBLOG_FAIL, + UNREBLOG_SUCCESS, + FAVOURITE_REQUEST, + FAVOURITE_SUCCESS, + FAVOURITE_FAIL, + UNFAVOURITE_SUCCESS +} from '../actions/interactions'; +import { + STATUS_FETCH_SUCCESS, + CONTEXT_FETCH_SUCCESS +} from '../actions/statuses'; +import { + TIMELINE_REFRESH_SUCCESS, + TIMELINE_UPDATE, + TIMELINE_DELETE, + TIMELINE_EXPAND_SUCCESS +} from '../actions/timelines'; +import { + ACCOUNT_TIMELINE_FETCH_SUCCESS, + ACCOUNT_TIMELINE_EXPAND_SUCCESS, + ACCOUNT_BLOCK_SUCCESS +} from '../actions/accounts'; +import { + NOTIFICATIONS_UPDATE, + NOTIFICATIONS_REFRESH_SUCCESS, + NOTIFICATIONS_EXPAND_SUCCESS +} from '../actions/notifications'; +import { + FAVOURITED_STATUSES_FETCH_SUCCESS, + FAVOURITED_STATUSES_EXPAND_SUCCESS +} from '../actions/favourites'; +import { SEARCH_FETCH_SUCCESS } from '../actions/search'; +import Immutable from 'immutable'; + +const normalizeStatus = (state, status) => { + if (!status) { + return state; + } + + const normalStatus = { ...status }; + normalStatus.account = status.account.id; + + if (status.reblog && status.reblog.id) { + state = normalizeStatus(state, status.reblog); + normalStatus.reblog = status.reblog.id; + } + + const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent; + + return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus))); +}; + +const normalizeStatuses = (state, statuses) => { + statuses.forEach(status => { + state = normalizeStatus(state, status); + }); + + return state; +}; + +const deleteStatus = (state, id, references) => { + references.forEach(ref => { + state = deleteStatus(state, ref[0], []); + }); + + return state.delete(id); +}; + +const filterStatuses = (state, relationship) => { + state.forEach(status => { + if (status.get('account') !== relationship.id) { + return; + } + + state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id'))); + }); + + return state; +}; + +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: + case FAVOURITED_STATUSES_FETCH_SUCCESS: + case FAVOURITED_STATUSES_EXPAND_SUCCESS: + case SEARCH_FETCH_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/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js @@ -0,0 +1,317 @@ +import { + TIMELINE_REFRESH_REQUEST, + TIMELINE_REFRESH_SUCCESS, + TIMELINE_REFRESH_FAIL, + TIMELINE_UPDATE, + TIMELINE_DELETE, + TIMELINE_EXPAND_SUCCESS, + TIMELINE_EXPAND_REQUEST, + TIMELINE_EXPAND_FAIL, + TIMELINE_SCROLL_TOP, + TIMELINE_CONNECT, + TIMELINE_DISCONNECT +} from '../actions/timelines'; +import { + REBLOG_SUCCESS, + UNREBLOG_SUCCESS, + FAVOURITE_SUCCESS, + UNFAVOURITE_SUCCESS +} from '../actions/interactions'; +import { + ACCOUNT_TIMELINE_FETCH_REQUEST, + ACCOUNT_TIMELINE_FETCH_SUCCESS, + ACCOUNT_TIMELINE_FETCH_FAIL, + ACCOUNT_TIMELINE_EXPAND_REQUEST, + ACCOUNT_TIMELINE_EXPAND_SUCCESS, + ACCOUNT_TIMELINE_EXPAND_FAIL, + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS +} from '../actions/accounts'; +import { + CONTEXT_FETCH_SUCCESS +} from '../actions/statuses'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + home: Immutable.Map({ + path: () => '/api/v1/timelines/home', + next: null, + isLoading: false, + online: false, + loaded: false, + top: true, + unread: 0, + items: Immutable.List() + }), + + public: Immutable.Map({ + path: () => '/api/v1/timelines/public', + next: null, + isLoading: false, + online: false, + loaded: false, + top: true, + unread: 0, + items: Immutable.List() + }), + + community: Immutable.Map({ + path: () => '/api/v1/timelines/public', + next: null, + params: { local: true }, + isLoading: false, + online: false, + loaded: false, + top: true, + unread: 0, + items: Immutable.List() + }), + + tag: Immutable.Map({ + path: (id) => `/api/v1/timelines/tag/${id}`, + next: null, + isLoading: false, + id: null, + loaded: false, + top: true, + unread: 0, + items: Immutable.List() + }), + + accounts_timelines: Immutable.Map(), + ancestors: Immutable.Map(), + descendants: Immutable.Map() +}); + +const normalizeStatus = (state, status) => { + const replyToId = status.get('in_reply_to_id'); + const id = status.get('id'); + + if (replyToId) { + if (!state.getIn(['descendants', replyToId], Immutable.List()).includes(id)) { + state = state.updateIn(['descendants', replyToId], Immutable.List(), set => set.push(id)); + } + + if (!state.getIn(['ancestors', id], Immutable.List()).includes(replyToId)) { + state = state.updateIn(['ancestors', id], Immutable.List(), set => set.push(replyToId)); + } + } + + return state; +}; + +const normalizeTimeline = (state, timeline, statuses, next) => { + let ids = Immutable.List(); + const loaded = state.getIn([timeline, 'loaded']); + + statuses.forEach((status, i) => { + state = normalizeStatus(state, status); + ids = ids.set(i, status.get('id')); + }); + + state = state.setIn([timeline, 'loaded'], true); + state = state.setIn([timeline, 'isLoading'], false); + + if (state.getIn([timeline, 'next']) === null) { + state = state.setIn([timeline, 'next'], next); + } + + return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? ids.concat(list) : ids)); +}; + +const appendNormalizedTimeline = (state, timeline, statuses, next) => { + let moreIds = Immutable.List(); + + statuses.forEach((status, i) => { + state = normalizeStatus(state, status); + moreIds = moreIds.set(i, status.get('id')); + }); + + state = state.setIn([timeline, 'isLoading'], false); + state = state.setIn([timeline, 'next'], next); + + return state.updateIn([timeline, 'items'], Immutable.List(), list => list.concat(moreIds)); +}; + +const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => { + let ids = Immutable.List(); + + statuses.forEach((status, i) => { + state = normalizeStatus(state, status); + ids = ids.set(i, status.get('id')); + }); + + return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map + .set('isLoading', false) + .set('loaded', true) + .set('next', true) + .update('items', Immutable.List(), list => (replace ? ids : ids.concat(list)))); +}; + +const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { + let moreIds = Immutable.List([]); + + statuses.forEach((status, i) => { + state = normalizeStatus(state, status); + moreIds = moreIds.set(i, status.get('id')); + }); + + return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map + .set('isLoading', false) + .set('next', next) + .update('items', list => list.concat(moreIds))); +}; + +const updateTimeline = (state, timeline, status, references) => { + const top = state.getIn([timeline, 'top']); + + state = normalizeStatus(state, status); + + if (!top) { + state = state.updateIn([timeline, 'unread'], unread => unread + 1); + } + + state = state.updateIn([timeline, 'items'], Immutable.List(), list => { + if (top && list.size > 40) { + list = list.take(20); + } + + if (list.includes(status.get('id'))) { + return list; + } + + const reblogOfId = status.getIn(['reblog', 'id'], null); + + if (reblogOfId !== null) { + list = list.filterNot(itemId => references.includes(itemId)); + } + + return list.unshift(status.get('id')); + }); + + return state; +}; + +const deleteStatus = (state, id, accountId, references, reblogOf) => { + if (reblogOf) { + // If we are deleting a reblog, just replace reblog with its original + return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item)); + } + + // Remove references from timelines + ['home', 'public', 'community', 'tag'].forEach(function (timeline) { + state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + }); + + // Remove references from account timelines + state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); + + // Remove references from context + state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { + state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + }); + + state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { + state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + }); + + state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); + + // Remove reblogs of deleted status + references.forEach(ref => { + state = deleteStatus(state, ref[0], ref[1], []); + }); + + return state; +}; + +const filterTimelines = (state, relationship, statuses) => { + let references; + + statuses.forEach(status => { + if (status.get('account') !== relationship.id) { + return; + } + + references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); + state = deleteStatus(state, status.get('id'), status.get('account'), references); + }); + + return state; +}; + +const normalizeContext = (state, id, ancestors, descendants) => { + const ancestorsIds = ancestors.map(ancestor => ancestor.get('id')); + const descendantsIds = descendants.map(descendant => descendant.get('id')); + + return state.withMutations(map => { + map.setIn(['ancestors', id], ancestorsIds); + map.setIn(['descendants', id], descendantsIds); + }); +}; + +const resetTimeline = (state, timeline, id) => { + if (timeline === 'tag' && typeof id !== 'undefined' && state.getIn([timeline, 'id']) !== id) { + state = state.update(timeline, map => map + .set('id', id) + .set('isLoading', true) + .set('loaded', false) + .set('next', null) + .set('top', true) + .update('items', list => list.clear())); + } else { + state = state.setIn([timeline, 'isLoading'], true); + } + + return state; +}; + +const updateTop = (state, timeline, top) => { + if (top) { + state = state.setIn([timeline, 'unread'], 0); + } + + return state.setIn([timeline, 'top'], top); +}; + +export default function timelines(state = initialState, action) { + switch(action.type) { + case TIMELINE_REFRESH_REQUEST: + case TIMELINE_EXPAND_REQUEST: + return resetTimeline(state, action.timeline, action.id); + case TIMELINE_REFRESH_FAIL: + case TIMELINE_EXPAND_FAIL: + return state.setIn([action.timeline, 'isLoading'], false); + case TIMELINE_REFRESH_SUCCESS: + return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + case TIMELINE_EXPAND_SUCCESS: + return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + case TIMELINE_UPDATE: + return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references); + case TIMELINE_DELETE: + return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); + case CONTEXT_FETCH_SUCCESS: + return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); + case ACCOUNT_TIMELINE_FETCH_REQUEST: + case ACCOUNT_TIMELINE_EXPAND_REQUEST: + return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); + case ACCOUNT_TIMELINE_FETCH_FAIL: + case ACCOUNT_TIMELINE_EXPAND_FAIL: + return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); + case ACCOUNT_TIMELINE_FETCH_SUCCESS: + return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); + case ACCOUNT_TIMELINE_EXPAND_SUCCESS: + return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterTimelines(state, action.relationship, action.statuses); + case TIMELINE_SCROLL_TOP: + return updateTop(state, action.timeline, action.top); + case TIMELINE_CONNECT: + return state.setIn([action.timeline, 'online'], true); + case TIMELINE_DISCONNECT: + return state.setIn([action.timeline, 'online'], false); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js @@ -0,0 +1,80 @@ +import { + FOLLOWERS_FETCH_SUCCESS, + FOLLOWERS_EXPAND_SUCCESS, + FOLLOWING_FETCH_SUCCESS, + FOLLOWING_EXPAND_SUCCESS, + FOLLOW_REQUESTS_FETCH_SUCCESS, + FOLLOW_REQUESTS_EXPAND_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS +} from '../actions/accounts'; +import { + REBLOGS_FETCH_SUCCESS, + FAVOURITES_FETCH_SUCCESS +} from '../actions/interactions'; +import { + BLOCKS_FETCH_SUCCESS, + BLOCKS_EXPAND_SUCCESS +} from '../actions/blocks'; +import { + MUTES_FETCH_SUCCESS, + MUTES_EXPAND_SUCCESS +} from '../actions/mutes'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + followers: Immutable.Map(), + following: Immutable.Map(), + reblogged_by: Immutable.Map(), + favourited_by: Immutable.Map(), + follow_requests: Immutable.Map(), + blocks: Immutable.Map(), + mutes: Immutable.Map() +}); + +const normalizeList = (state, type, id, accounts, next) => { + return state.setIn([type, id], Immutable.Map({ + next, + items: Immutable.List(accounts.map(item => item.id)) + })); +}; + +const appendToList = (state, type, id, accounts, next) => { + return state.updateIn([type, id], map => { + return map.set('next', next).update('items', list => list.concat(accounts.map(item => item.id))); + }); +}; + +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_REQUESTS_EXPAND_SUCCESS: + return state.updateIn(['follow_requests', 'items'], list => list.concat(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)); + case BLOCKS_FETCH_SUCCESS: + return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + case BLOCKS_EXPAND_SUCCESS: + return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + case MUTES_FETCH_SUCCESS: + return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + case MUTES_EXPAND_SUCCESS: + return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/rtl.js b/app/javascript/mastodon/rtl.js @@ -0,0 +1,27 @@ +// U+0590 to U+05FF - Hebrew +// U+0600 to U+06FF - Arabic +// U+0700 to U+074F - Syriac +// U+0750 to U+077F - Arabic Supplement +// U+0780 to U+07BF - Thaana +// U+07C0 to U+07FF - N'Ko +// U+0800 to U+083F - Samaritan +// U+08A0 to U+08FF - Arabic Extended-A +// U+FB1D to U+FB4F - Hebrew presentation forms +// U+FB50 to U+FDFF - Arabic presentation forms A +// U+FE70 to U+FEFF - Arabic presentation forms B + +const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; + +export function isRtl(text) { + if (text.length === 0) { + return false; + } + + const matches = text.match(rtlChars); + + if (!matches) { + return false; + } + + return matches.length / text.trim().length > 0.3; +}; diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js @@ -0,0 +1,73 @@ +import { createSelector } from 'reselect'; +import Immutable from 'immutable'; + +const getStatuses = state => state.get('statuses'); +const getAccounts = state => state.get('accounts'); + +const getAccountBase = (state, id) => state.getIn(['accounts', id], null); +const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); +const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); + +export const makeGetAccount = () => { + return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => { + if (base === null) { + return null; + } + + return base.merge(counters).set('relationship', relationship); + }); +}; + +export const makeGetStatus = () => { + return createSelector( + [ + (state, id) => state.getIn(['statuses', id]), + (state, id) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), + (state, id) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), + (state, id) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), + ], + + (statusBase, statusReblog, accountBase, accountReblog) => { + if (!statusBase) { + return null; + } + + if (statusReblog) { + statusReblog = statusReblog.set('account', accountReblog); + } else { + statusReblog = null; + } + + return statusBase.withMutations(map => { + map.set('reblog', statusReblog); + map.set('account', accountBase); + }); + } + ); +}; + +const getAlertsBase = state => state.get('alerts'); + +export const getAlerts = createSelector([getAlertsBase], (base) => { + let arr = []; + + base.forEach(item => { + arr.push({ + message: item.get('message'), + title: item.get('title'), + key: item.get('key'), + dismissAfter: 5000 + }); + }); + + return arr; +}); + +export const makeGetNotification = () => { + return createSelector([ + (_, base) => base, + (state, _, accountId) => state.getIn(['accounts', accountId]) + ], (base, account) => { + return base.set('account', account); + }); +}; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js @@ -0,0 +1,16 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import appReducer from '../reducers'; +import loadingBarMiddleware from '../middleware/loading_bar'; +import errorsMiddleware from '../middleware/errors'; +import soundsMiddleware from '../middleware/sounds'; +import Immutable from 'immutable'; + +export default function configureStore() { + return createStore(appReducer, compose(applyMiddleware( + thunk, + loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), + errorsMiddleware(), + soundsMiddleware() + ), window.devToolsExtension ? window.devToolsExtension() : f => f)); +}; diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js @@ -0,0 +1,22 @@ +import WebSocketClient from 'websocket.js'; + +const createWebSocketURL = (url) => { + const a = document.createElement('a'); + + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace('http', 'ws'); + + return a.href; +}; + +export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { + const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); + + ws.onopen = connected; + ws.onmessage = e => received(JSON.parse(e.data)); + ws.onclose = disconnected; + ws.onreconnect = reconnected; + + return ws; +}; diff --git a/app/javascript/mastodon/uuid.js b/app/javascript/mastodon/uuid.js @@ -0,0 +1,3 @@ +export default function uuid(a) { + return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid); +}; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js @@ -0,0 +1,29 @@ +import Mastodon from 'mastodon/containers/mastodon'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import 'font-awesome/css/font-awesome.css'; +import '../styles/application.scss'; + +if (!window.Intl) { + require('intl'); + require('intl/locale-data/jsonp/en.js'); +} + +window.jQuery = window.$ = require('jquery'); +window.Perf = require('react-addons-perf'); + +require('jquery-ujs'); +require.context('../images/', true); + +const customContext = require.context('../../assets/stylesheets/', false); + +if (customContext.keys().indexOf('./custom.scss') !== -1) { + customContext('./custom.scss'); +} + +document.addEventListener('DOMContentLoaded', () => { + const mountNode = document.getElementById('mastodon'); + const props = JSON.parse(mountNode.getAttribute('data-props')); + + ReactDOM.render(<Mastodon {...props} />, mountNode); +}); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js @@ -0,0 +1,53 @@ +import emojify from 'mastodon/emoji'; +import { length } from 'stringz'; + +window.jQuery = window.$ = require('jquery'); +require('jquery-ujs'); +require.context('../images/', true); + +$(() => { + $.each($('.emojify'), (_, content) => { + const $content = $(content); + $content.html(emojify($content.html())); + }); + + $('.video-player video').on('click', e => { + if (e.target.paused) { + e.target.play(); + } else { + e.target.pause(); + } + }); + + $('.media-spoiler').on('click', e => { + $(e.target).hide(); + }); + + $('.webapp-btn').on('click', e => { + if (e.button === 0) { + e.preventDefault(); + window.location.href = $(e.target).attr('href'); + } + }); + + $('.status__content__spoiler-link').on('click', e => { + e.preventDefault(); + const contentEl = $(e.target).parent().parent().find('div'); + + if (contentEl.is(':visible')) { + contentEl.hide(); + $(e.target).parent().attr('style', 'margin-bottom: 0'); + } else { + contentEl.show(); + $(e.target).parent().attr('style', null); + } + }); + + $('.account_display_name').on('input', e => { + $('.name-counter').text(30 - length($(e.target).val())); + }); + + $('.account_note').on('input', e => { + $('.note-counter').text(160 - length($(e.target).val())); + }); +}); diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss @@ -0,0 +1,374 @@ +.about-body { + .wrapper { + max-width: 600px; + margin: 0 auto; + color: $color3; + padding-top: 50px; + padding-bottom: 50px; + + &.thicc { + max-width: 700px; + } + } + + h1 { + font: 46px/52px 'Roboto', sans-serif; + font-weight: 600; + margin-bottom: 20px; + color: $color4; + padding: 20px 0; + + img { + margin-bottom: -5px; + margin-right: 5px; + width: 46px; + height: 46px; + } + } + + h2 { + font-family: 'Montserrat', sans-serif; + font-size: 24px; + line-height: 28px; + font-weight: 400; + margin-bottom: 20px; + color: $color5; + } + + h3 { + font-family: 'Montserrat', sans-serif; + font-size: 20px; + line-height: 28px; + font-weight: 400; + margin-bottom: 20px; + color: $color2; + } + + ul, ol { + list-style: inherit; + margin-left: 20px; + + &[type='a'] { + list-style-type: lower-alpha; + } + + &[type='i'] { + list-style-type: lower-roman; + } + } + + li > ol, li > ul { + margin-top: 20px; + } + + p, li { + font: 16px/28px 'Montserrat', sans-serif; + font-weight: 400; + margin-bottom: 12px; + + a { + color: $color4; + text-decoration: underline; + } + } + + em { + display: inline-block; + padding: 7px 7px 5px 7px; + margin: 0 2px; + background: $color3; + color: $color1; + font: 16px/16px 'Montserrat', sans-serif; + font-weight: 300; + } + + .screenshot { + box-shadow: 0 0 15px rgba($color8, 0.4); + margin-bottom: 26px; + + img { + max-width: 100%; + height: auto; + display: block; + } + } + + .actions { + overflow: hidden; + margin-bottom: 20px; + + .info { + float: right; + text-align: right; + line-height: 36px; + + a { + color: $color3; + text-decoration: underline; + } + } + } + + @media screen and (max-width: 625px) { + .wrapper { + padding: 20px; + } + + .features-list { + display: block; + } + } +} + +.information-board { + margin: 20px 0; + display: flex; + justify-content: space-between; + border-top: 1px solid lighten($color1, 10%); + border-bottom: 1px solid lighten($color1, 10%); + padding-right: 14px; + + .section { + flex: 1 0 0; + padding: 14px; + text-align: right; + font: 16px/28px 'Montserrat', sans-serif; + + span, strong { + display: block; + } + + span { + font-size: 16px; + + &:last-child { + color: $color2; + font-size: 14px; + } + } + + strong { + font-weight: 500; + font-size: 32px; + line-height: 48px; + color: $color5; + } + } + + @media screen and (max-width: 500px) { + flex-direction: column; + + .section { + text-align: left; + } + } +} + +.owner { + text-align: center; + + .avatar { + width: 80px; + height: 80px; + margin: 0 auto; + margin-bottom: 15px; + + img { + display: block; + width: 80px; + height: 80px; + border-radius: 48px; + } + } + + .name { + font-size: 14px; + + a { + display: block; + color: $color5; + text-decoration: none; + + &:hover { + .display_name { + text-decoration: underline; + } + } + } + + .username { + display: block; + color: $color3; + } + } +} + +.contact-email { + text-align: center; + margin: 40px 0; + + strong { + display: block; + color: $color5; + word-break: break-word; + } +} + +.sidebar-layout { + display: flex; + + .main { + flex: 1 1 auto; + padding: 14px 0; + + .panel { + padding-right: 14px; + } + } + + .sidebar { + border-left: 1px solid lighten($color1, 10%); + width: 180px; + flex: 0 0 auto; + } + + .panel { + .panel-header { + background: lighten($color1, 10%); + padding: 7px 14px; + text-transform: uppercase; + font-size: 12px; + font-weight: 500; + } + + .panel-body { + padding: 14px; + } + + .panel-list { + ul { + list-style: none; + margin: 0; + + li { + margin: 0; + font-family: inherit; + font-size: 13px; + line-height: 18px; + + a { + display: block; + padding: 7px 14px; + color: rgba($color5, 0.7); + text-decoration: none; + transition: all 200ms linear; + + i.fa { + margin-right: 5px; + } + + &:hover { + color: $color5; + background-color: darken($color1, 5%); + transition: all 100ms linear; + } + + &.selected { + color: $color5; + background-color: $color4; + + &:hover { + background-color: lighten($color4, 5%); + } + } + } + } + } + } + } + + @media screen and (max-width: 625px) { + flex-direction: column; + + .sidebar { + border: 1px solid lighten($color1, 10%); + width: auto; + } + } +} + +.features-list { + display: flex; + margin-bottom: 20px; + + .features-list__column { + flex: 1 1 0; + + ul { + list-style: none; + } + + li { + margin: 0; + } + } +} + +.screenshot-with-signup { + display: flex; + margin-bottom: 20px; + + .mascot { + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + img { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; + } + } + + .simple_form, .closed-registrations-message { + width: 300px; + flex: 0 0 auto; + background: rgba(darken($color1, 7%), 0.5); + padding: 14px; + border-radius: 4px; + box-shadow: 0 0 15px rgba($color8, 0.4); + + .actions { + margin-bottom: 0; + } + + .info { + text-align: center; + + a { + color: $color2; + } + } + } + + @media screen and (max-width: 625px) { + .mascot { + display: none; + } + + .simple_form, .closed-registrations-message { + flex: auto; + } + } +} + +.closed-registrations-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss @@ -0,0 +1,391 @@ +.card { + background: $color1; + background-size: cover; + padding: 60px 0; + padding-bottom: 0; + border-radius: 4px 4px 0 0; + box-shadow: 0 0 15px rgba($color8, 0.2); + overflow: hidden; + position: relative; + + @media screen and (max-width: 700px) { + border-radius: 0; + box-shadow: none; + } + + &:after { + background: linear-gradient(rgba($color8, 0.5), rgba($color8, 0.8)); + display: block; + content: ""; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + } + + .name { + display: block; + font-size: 20px; + line-height: 18px * 1.5; + color: $color5; + font-weight: 500; + text-align: center; + position: relative; + z-index: 2; + text-shadow: 0 0 2px $color8; + + small { + display: block; + font-size: 14px; + color: $color4; + font-weight: 400; + } + } + + .avatar { + width: 120px; + margin: 0 auto; + margin-bottom: 15px; + position: relative; + z-index: 2; + + img { + width: 120px; + height: 120px; + display: block; + border-radius: 120px; + } + } + + .controls { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + } + + .details { + display: flex; + margin-top: 30px; + position: relative; + z-index: 2; + flex-direction: row; + } + + .details-counters { + display: flex; + flex-direction: row; + order: 0; + } + + .counter { + width: 80px; + color: $color3; + padding: 5px 10px 0px; + margin-bottom: 10px; + border-right: 1px solid $color3; + cursor: default; + position: relative; + + a { + display: block; + } + + &:after { + display: block; + content: ""; + position: absolute; + bottom: -10px; + left: 0; + width: 100%; + border-bottom: 4px solid $color3; + opacity: 0.5; + transition: all 0.8s ease; + } + + &.active { + &:after { + border-bottom: 4px solid $color4; + opacity: 1; + } + } + + &:hover { + &:after { + opacity: 1; + transition-duration: 0.2s; + } + } + + a { + text-decoration: none; + color: inherit; + } + + .counter-label { + font-size: 12px; + text-transform: uppercase; + display: block; + margin-bottom: 5px; + text-shadow: 0 0 2px $color8; + } + + .counter-number { + font-weight: 500; + font-size: 18px; + color: $color5; + } + } + + .bio { + flex: 1; + font-size: 14px; + line-height: 18px; + padding: 5px 10px; + color: $color2; + order: 1; + } + + @media screen and (max-width: 480px) { + .details { + display: block; + } + + .bio { + text-align: center; + margin-bottom: 20px; + } + + .counter { + flex: 1 1 auto; + } + + .counter:last-child { + border-right: none; + } + } +} + +.pagination { + padding: 30px 0; + text-align: center; + overflow: hidden; + + a, .current, .next, .prev, .page, .gap { + font-size: 14px; + color: $color5; + font-weight: 500; + display: inline-block; + padding: 6px 10px; + text-decoration: none; + } + + .current { + background: $color5; + border-radius: 100px; + color: $color1; + cursor: default; + margin: 0 10px; + } + + .gap { + cursor: default; + } + + .prev, .next { + text-transform: uppercase; + color: $color2; + } + + .prev { + float: left; + padding-left: 0; + + .fa { + display: inline-block; + margin-right: 5px; + } + } + + .next { + float: right; + padding-right: 0; + + .fa { + display: inline-block; + margin-left: 5px; + } + } + + .disabled { + cursor: default; + color: lighten($color1, 10%); + } + + @media screen and (max-width: 360px) { + padding: 30px 20px; + + a, .current, .next, .prev, .gap { + display: none; + } + + .next, .prev { + display: inline-block; + } + } +} + +.accounts-grid { + box-shadow: 0 0 15px rgba($color8, 0.2); + background: $color5; + border-radius: 0 0 4px 4px; + padding: 20px 10px; + padding-bottom: 10px; + overflow: hidden; + display: flex; + flex-wrap: wrap; + + @media screen and (max-width: 700px) { + border-radius: 0; + box-shadow: none; + } + + .account-grid-card { + box-sizing: border-box; + width: 335px; + border: 1px solid $color2; + border-radius: 4px; + color: $color1; + margin-bottom: 10px; + + &:nth-child(odd) { + margin-right: 10px; + } + + .account-grid-card__header { + overflow: hidden; + padding: 10px; + border-bottom: 1px solid $color2; + } + + .avatar { + width: 60px; + height: 60px; + float: left; + margin-right: 15px; + + img { + display: block; + width: 60px; + height: 60px; + border-radius: 60px; + } + } + + .name { + padding-top: 10px; + + a { + display: block; + color: $color1; + text-decoration: none; + + &:hover { + .display_name { + text-decoration: underline; + } + } + } + } + + .display_name { + font-size: 14px; + display: block; + } + + .username { + color: $color4; + } + + .note { + padding: 10px; + padding-top: 15px; + color: $color3; + word-wrap: break-word; + } + } +} + +.nothing-here { + color: $color3; + font-size: 14px; + font-weight: 500; + text-align: center; + padding: 15px 0; + padding-bottom: 25px; + cursor: default; +} + +.account-card { + padding: 14px 10px; + background: $color5; + border-radius: 4px; + text-align: left; + box-shadow: 0 0 15px rgba($color8, 0.2); + + .detailed-status__display-name { + display: block; + overflow: hidden; + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + + & > div { + float: left; + margin-right: 10px; + width: 48px; + height: 48px; + } + + .avatar { + display: block; + border-radius: 4px; + } + + .display-name { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: default; + + strong { + font-weight: 500; + color: $color1; + } + + span { + font-size: 14px; + color: $color3; + } + } + + &:hover { + .display-name { + strong { + text-decoration: none; + } + } + } + } + + .account__header__content { + font-size: 14px; + color: $color1; + } +} diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/admin.scss @@ -0,0 +1,245 @@ +.admin-wrapper { + display: flex; + justify-content: center; + height: 100%; + + .sidebar-wrapper { + flex: 1; + height: 100%; + background: $color1; + display: flex; + justify-content: flex-end; + } + + .sidebar { + width: 240px; + height: 100%; + padding: 0; + overflow-y: auto; + + .logo { + display: block; + margin: 40px auto; + width: 100px; + height: 100px; + } + + ul { + list-style: none; + border-radius: 4px 0 0 4px; + overflow: hidden; + margin-bottom: 20px; + + a { + display: block; + padding: 15px 25px; + color: rgba($color5, 0.7); + text-decoration: none; + transition: all 200ms linear; + border-radius: 4px 0 0 4px; + + i.fa { + margin-right: 5px; + } + + &:hover { + color: $color5; + background-color: darken($color1, 5%); + transition: all 100ms linear; + } + + &.selected { + background: darken($color1, 2%); + border-radius: 4px 0 0 0; + } + } + + ul { + background: darken($color1, 4%); + border-radius: 0 0 0 4px; + margin: 0; + + a { + border: 0; + + &.selected { + color: $color5; + background-color: $color4; + border-bottom: 0; + border-radius: 0; + + &:hover { + background-color: lighten($color4, 5%); + } + } + } + } + } + } + + .content-wrapper { + flex: 2; + overflow: auto; + } + + .content { + max-width: 700px; + padding: 20px 15px; + padding-top: 60px; + padding-left: 25px; + + h2 { + color: $color2; + font-size: 24px; + line-height: 28px; + font-weight: 400; + margin-bottom: 40px; + } + + & > p { + font-size: 14px; + line-height: 18px; + color: $color2; + margin-bottom: 20px; + + strong { + color: $color5; + font-weight: 500; + } + } + + hr { + margin: 20px 0; + border: 0; + background: transparent; + border-bottom: 1px solid $color1; + } + } + + .simple_form { + max-width: 400px; + .label_input { + label.select { + width: 50%; + } + select { + width: 50%; + float: right; + } + } + } + + @media screen and (max-width: 600px) { + display: block; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + + .sidebar-wrapper, .content-wrapper { + flex: 0 0 auto; + height: auto; + overflow: initial; + } + + .sidebar { + width: 100%; + padding: 10px 0; + height: auto; + + .logo { + margin: 20px auto; + } + } + + .content { + padding-top: 20px; + } + } +} + +.filters { + display: flex; + margin-bottom: 20px; + + .filter-subset { + flex: 0 0 auto; + margin-right: 40px; + + ul { + margin-top: 5px; + list-style: none; + + li { + display: inline-block; + margin-right: 5px; + } + } + + strong { + font-weight: 500; + text-transform: uppercase; + font-size: 12px; + } + + a { + display: inline-block; + color: rgba($color5, 0.7); + text-decoration: none; + text-transform: uppercase; + font-size: 12px; + font-weight: 500; + border-bottom: 2px solid $color1; + + &:hover { + color: $color5; + border-bottom: 2px solid lighten($color1, 5%); + } + + &.selected { + color: $color4; + border-bottom: 2px solid $color4; + } + } + } +} + +.report-accounts { + display: flex; + margin-bottom: 20px; +} + +.report-accounts__item { + flex: 1 1 0; + display: flex; + flex-direction: column; + + & > strong { + display: block; + margin-bottom: 10px; + font-weight: 500; + font-size: 14px; + line-height: 18px; + color: $color2; + } + + &:first-child { + margin-right: 10px; + } + + .account-card { + flex: 1 1 auto; + } +} + +.report-status { + display: flex; + margin-bottom: 10px; + + .activity-stream { + flex: 2 0 0; + margin-right: 20px; + } +} + +.report-status__actions { + flex: 0 0 auto; +} diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss @@ -0,0 +1,20 @@ +@import 'variables'; +@import 'fonts/roboto'; +@import 'fonts/roboto-mono'; +@import 'fonts/montserrat'; + +@import 'reset'; +@import 'basics'; +@import 'containers'; +@import 'lists'; +@import 'footer'; +@import 'compact_header'; +@import 'landing_strip'; +@import 'forms'; +@import 'accounts'; +@import 'stream_entries'; +@import 'components'; +@import 'about'; +@import 'tables'; +@import 'admin'; +@import 'rtl'; diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss @@ -0,0 +1,58 @@ +body { + font-family: 'Roboto', sans-serif; + background: $color1 url('../images/background-photo.jpg'); + background-size: cover; + background-attachment: fixed; + font-size: 13px; + line-height: 18px; + font-weight: 400; + color: $color5; + padding-bottom: 140px; + text-rendering: optimizelegibility; + font-feature-settings: "kern"; + text-size-adjust: none; + + &.app-body { + position: fixed; + width: 100%; + height: 100%; + padding: 0; + background: $color1; + } + + &.embed { + background: transparent; + margin: 0; + + .container { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + } + } + + &.admin { + background: darken($color1, 4%); + position: fixed; + width: 100%; + height: 100%; + padding: 0; + } + + @media screen and (max-width: 360px) { + padding-bottom: 0; + } +} + +button:focus { + outline: none; +} + +.app-holder { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss @@ -0,0 +1,11 @@ +@function url-friendly-colour($colour) { + @return '%23' + str-slice('#{$colour}', 2, -1) +} + +button.icon-button i.fa-retweet { + background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($color1, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($color4)}' stroke-width='0'/></svg>"); + + &:hover { + background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($color1, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($color4)}' stroke-width='0'/></svg>"); + } +} diff --git a/app/javascript/styles/compact_header.scss b/app/javascript/styles/compact_header.scss @@ -0,0 +1,28 @@ +.compact-header { + h1 { + font-size: 24px; + line-height: 28px; + color: $color3; + overflow: hidden; + font-weight: 500; + margin-bottom: 20px; + + a { + color: inherit; + text-decoration: none; + } + + small { + font-weight: 400; + color: $color2; + } + + img { + display: inline-block; + margin-bottom: -5px; + margin-right: 15px; + width: 36px; + height: 36px; + } + } +} diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss @@ -0,0 +1,3189 @@ +@import 'variables'; + +.app-body { + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; +} + +.button { + background-color: darken($color4, 3%); + border: 10px none; + border-radius: 4px; + box-sizing: border-box; + color: $color5; + cursor: pointer; + display: inline-block; + font-family: inherit; + font-size: 14px; + font-weight: 500; + height: 36px; + letter-spacing: 0; + line-height: 36px; + overflow: hidden; + padding: 0 16px; + position: relative; + text-align: center; + text-transform: uppercase; + text-decoration: none; + text-overflow: ellipsis; + transition: all 100ms ease-in; + white-space: nowrap; + + &:active, + &:focus, + &:hover { + background-color: lighten($color4, 7%); + transition: all 200ms ease-out; + } + + &:disabled { + background-color: $color3; + cursor: default; + } + + &.button-secondary { + // + } +} + +.column-collapsable { + position: relative; +} + +.column-icon { + background: lighten($color1, 4%); + color: $color3; + cursor: pointer; + font-size: 16px; + padding: 15px; + position: absolute; + right: 0; + top: -48px; + z-index: 3; + + &:hover { + color: lighten($color3, 7%); + } +} + +.column-icon-clear { + font-size: 16px; + padding: 15px; + position: absolute; + right: 48px; + top: 0; + cursor: pointer; + z-index: 2; +} + +@media screen and (min-width: 1025px) { + .column-icon-clear { + top: 10px; + } +} + +.icon-button { + display: inline-block; + padding: 0; + color: lighten($color1, 26%); + border: none; + background: transparent; + cursor: pointer; + transition: color 100ms ease-in; + + &:hover, &:active, &:focus { + color: lighten($color1, 33%); + transition: color 200ms ease-out; + } + + &.disabled { + color: lighten($color1, 13%); + cursor: default; + } + + &.active { + color: $color4; + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, &:focus, &:active { + outline: 0 !important; + } + + &.inverted { + color: lighten($color1, 33%); + + &:hover, &:active, &:focus { + color: lighten($color1, 26%); + } + + &.active { + color: $color4; + } + + &.disabled { + color: $color3; + } + } + + &.overlayed { + box-sizing: content-box; + background: rgba($color8, 0.6); + color: rgba($color5, 0.7); + border-radius: 4px; + padding: 2px; + + &:hover { + background: rgba($color8, 0.9); + } + } +} + +.text-icon-button { + color: lighten($color1, 33%); + border: none; + background: transparent; + cursor: pointer; + font-weight: 600; + font-size: 11px; + padding: 0 3px; + line-height: 27px; + outline: 0; + transition: color 100ms ease-in; + + &:hover, &:active, &:focus { + color: lighten($color1, 26%); + transition: color 200ms ease-out; + } + + &.disabled { + color: lighten($color1, 13%); + cursor: default; + } + + &.active { + color: $color4; + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, &:focus, &:active { + outline: 0 !important; + } +} + +.dropdown--active .icon-button { + color: $color4; +} + +.dropdown--active:after { + content: ""; + display: block; + position: absolute; + width: 0; + height: 0; + border-style: solid; + border-width: 0 4.5px 7.8px 4.5px; + border-color: transparent transparent $color2 transparent; + bottom: 8px; + right: 104px; +} + +.invisible { + font-size: 0; + line-height: 0; + display: inline-block; + width: 0; +} + +.ellipsis { + &:after { + content: "…"; + } +} + +.lightbox .icon-button { + color: $color1; +} + +.compose-form { + padding: 10px; +} + +.compose-form__warning { + color: darken($color3, 33%); + margin-bottom: 15px; + background: $color3; + box-shadow: 0 2px 6px rgba($color8, 0.3); + padding: 8px 10px; + border-radius: 4px; + font-size: 13px; + font-weight: 400; + + strong { + color: darken($color3, 33%); + font-weight: 500; + } + + a { + color: darken($color3, 33%); + font-weight: 500; + text-decoration: underline; + + &:hover, &:active, &:focus { + text-decoration: none; + } + } +} + +.compose-form__modifiers { + color: $color1; + font-family: inherit; + font-size: 14px; + background: $color5; + border-radius: 0 0 4px 0; +} + +.compose-form__buttons-wrapper { + display: flex; + justify-content: space-between; +} + +.compose-form__buttons { + padding: 10px; + background: darken($color5, 8%); + box-shadow: inset 0 5px 5px rgba($color8, 0.05); + border-radius: 0 0 4px 4px; + display: flex; + + .icon-button { + box-sizing: content-box; + padding: 0 3px; + } +} + +.compose-form__upload-button-icon { + line-height: 27px; +} + +.compose-form__upload-wrapper { + overflow: hidden; +} + +.compose-form__uploads-wrapper { + display: flex; + padding: 5px; +} + +.compose-form__upload { + flex: 1 1 0; + margin: 5px; +} + +.compose-form__upload-thumbnail { + border-radius: 4px; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + height: 100px; + width: 100%; +} + +.compose-form__upload-cancel { + background-size: cover; + border-radius: 4px; + height: 100px; + width: 100px; +} + +.compose-form__label { + display: block; + line-height: 24px; + vertical-align: middle; + + &.with-border { + border-top: 1px solid $color1; + padding-top: 10px; + } + + .compose-form__label__text { + display: inline-block; + vertical-align: middle; + margin-bottom: 14px; + margin-left: 8px; + color: $color3; + } +} + +.compose-form__textarea, .follow-form__input { + background: $color5; + + &:disabled { + background: $color2; + } +} + +.compose-form__autosuggest-wrapper { + position: relative; + + .dropdown--active:after { + border-color: transparent transparent $color5 transparent; + bottom: -1px; + right: 8px; + } +} + +.compose-form__publish { + display: flex; + min-width: 0; +} + +.compose-form__publish-button-wrapper { + overflow: hidden; + padding-top: 10px; +} + +.emojione { + display: inline-block; + font-size: inherit; + vertical-align: middle; + margin: -.2ex .15em .2ex; + width: 16px; + height: 16px; + + img { + width: auto; + } +} + +.reply-indicator { + border-radius: 4px 4px 0 0; + position: relative; + bottom: -2px; + background: $color3; + padding: 10px; +} + +.reply-indicator__header { + margin-bottom: 5px; + overflow: hidden; +} + +.reply-indicator__cancel { + float: right; + line-height: 24px; +} + +.reply-indicator__display-name { + color: $color1; + display: block; + max-width: 100%; + line-height: 24px; + overflow: hidden; + padding-right: 25px; + text-decoration: none; +} + +.reply-indicator__display-avatar { + float: left; + margin-right: 5px; +} + +.status__content { + cursor: pointer; +} + +.status__content--no-action { + cursor: default; +} + +.status__content, +.reply-indicator__content { + font-size: 15px; + line-height: 20px; + word-wrap: break-word; + font-weight: 400; + overflow: hidden; + white-space: pre-wrap; + + .emojione { + width: 18px; + height: 18px; + } + + p { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $color2; + text-decoration: none; + + &:hover { + text-decoration: underline; + + .fa { + color: lighten($color1, 40%); + } + } + + &.mention { + &:hover { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + + .fa { + color: lighten($color1, 30%); + } + } + + .status__content__spoiler-link { + background: lighten($color1, 30%); + + &:hover { + background: lighten($color1, 33%); + text-decoration: none; + } + } +} + +a.status__content__spoiler-link { + display: inline-block; + border-radius: 2px; + color: lighten($color1, 8%); + font-weight: 500; + font-size: 11px; + padding: 0px 6px; + text-transform: uppercase; + line-height: inherit; +} + +.status__prepend-icon-wrapper { + left: -26px; + position: absolute; +} + +.status { + padding: 8px 10px; + padding-left: 68px; + position: relative; + min-height: 48px; + border-bottom: 1px solid lighten($color1, 8%); + cursor: default; + + &.light { + .status__relative-time { + color: $color3; + } + + .status__display-name { + color: $color1; + } + + .display-name { + strong { + color: $color1; + } + + span { + color: $color3; + } + } + + .status__content { + color: $color1; + + a { + color: $color4; + } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } + } + } +} + +.status__relative-time { + color: lighten($color1, 26%); +} + +.status__display-name { + color: lighten($color1, 26%); +} + +.status__info .status__display-name { + display: block; + max-width: 100%; + padding-right: 25px; +} + +.status__info { + font-size: 15px; +} + +.status__info-time { + float: right; + font-size: 14px; +} + +.status-check-box { + border-bottom: 1px solid lighten($color1, 8%); + display: flex; + + .status__content { + background: lighten($color1, 4%); + flex: 1 1 auto; + padding: 10px; + } +} + +.status-check-box-toggle { + align-items: center; + display: flex; + flex: 0 0 auto; + justify-content: center; + padding: 10px; +} + +.status__prepend { + margin-left: 68px; + color: lighten($color1, 26%); + padding: 8px 0; + padding-bottom: 2px; + font-size: 14px; + position: relative; + + .status__display-name strong { + color: lighten($color1, 26%); + } +} + +.status__action-bar { + align-items: center; + display: flex; + margin-top: 10px; +} + +.status__action-bar-button-wrapper { + float: left; + margin-right: 18px; +} + +.status__action-bar-dropdown { + float: left; + height: 18px; + width: 18px; +} + +.detailed-status { + background: lighten($color1, 4%); + padding: 14px 10px; + + .status__content { + font-size: 19px; + line-height: 24px; + + .emojione { + width: 22px; + height: 22px; + } + } +} + +.detailed-status__meta { + margin-top: 15px; + color: lighten($color1, 26%); + font-size: 14px; + line-height: 18px; +} + +.detailed-status__action-bar { + background: lighten($color1, 4%); + border-top: 1px solid lighten($color1, 8%); + border-bottom: 1px solid lighten($color1, 8%); + display: flex; + flex-direction: row; + padding: 10px 0; +} + +.detailed-status__link { + color: inherit; + text-decoration: none; +} + +.detailed-status__favorites, +.detailed-status__reblogs { + display: inline-block; + font-weight: 500; + font-size: 12px; + margin-left: 6px; +} + +.reply-indicator__content { + color: $color1; + font-size: 14px; + + a { + color: lighten($color1, 20%); + } +} + +.account { + padding: 10px; + border-bottom: 1px solid lighten($color1, 8%); + + .account__display-name { + flex: 1 1 auto; + display: block; + color: $color3; + overflow: hidden; + text-decoration: none; + font-size: 14px; + } +} + +.account__wrapper { + display: flex; +} + +.account__avatar-wrapper { + float: left; + margin-left: 12px; + margin-right: 12px; +} + +.account__avatar { + border-radius: 4px; + background: transparent no-repeat; + background-position: 50%; + background-clip: padding-box; + position: relative; +} + +.account__relationship { + height: 18px; + padding: 10px; +} + +.account__header { + flex: 0 0 auto; + background: lighten($color1, 4%); + text-align: center; + background-size: cover; + background-position: center; + position: relative; + + & > div { + background: rgba(lighten($color1, 4%), 0.9); + } + + .account__header__content { + color: $color2; + } + + .account__header__display-name { + color: $color5; + } + + .account__header__username { + color: $color4; + } +} + +.account__header__content { + color: $color3; + font-size: 14px; + font-weight: 400; + overflow: hidden; + word-break: normal; + word-wrap: break-word; + + p { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: inherit; + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } +} + +.account__header__display-name { + .emojione { + width: 25px; + height: 25px; + } +} + +.account__action-bar { + border-top: 1px solid lighten($color1, 8%); + border-bottom: 1px solid lighten($color1, 8%); + line-height: 36px; + overflow: hidden; + flex: 0 0 auto; + display: flex; +} + +.account__action-bar-dropdown { + flex: 1 1 auto; + padding: 10px; + + .dropdown--active { + .dropdown__content.dropdown__right { + left: 6px; + right: initial; + } + + &:after { + bottom: initial; + margin-left: 11px; + margin-top: -7px; + right: initial; + } + } +} + +.account__action-bar-links { + display: flex; + flex: 1 1 auto; + line-height: 18px; +} + +.account__action-bar__tab { + text-decoration: none; + overflow: hidden; + width: 80px; + border-left: 1px solid lighten($color1, 8%); + padding: 10px 5px; + + & > span { + display: block; + text-transform: uppercase; + font-size: 11px; + color: $color3; + } + + strong { + display: block; + font-size: 15px; + font-weight: 500; + color: $color5; + } + + abbr { + color: lighten($color1, 26%); + } +} + +.account__header__avatar { + background-size: 90px 90px; + display: block; + height: 90px; + margin: 0 auto 10px; + overflow: hidden; + width: 90px; +} + +.account-authorize { + padding: 14px 10px; + + .detailed-status__display-name { + display: block; + margin-bottom: 15px; + overflow: hidden; + } +} + +.account-authorize__avatar { + float: left; + margin-right: 10px; +} + +.status__display-name, +.status__relative-time, +.detailed-status__display-name, +.detailed-status__datetime, +.detailed-status__application, +.account__display-name { + text-decoration: none; +} + +.status__display-name, +.account__display-name { + strong { + color: $color5; + } + + &.muted { + .emojione { + opacity: 0.5; + } + } +} + +.status__display-name, +.reply-indicator__display-name, +.detailed-status__display-name, +.account__display-name { + &:hover strong { + text-decoration: underline; + } +} + +.account__display-name strong { + display: block; +} + +.detailed-status__application, +.detailed-status__datetime { + color: inherit; +} + +.detailed-status__display-name { + color: $color2; + display: block; + line-height: 24px; + margin-bottom: 15px; + overflow: hidden; + + strong, + span { + display: block; + } + + strong { + font-size: 16px; + color: $color5; + } +} + +.detailed-status__display-avatar { + float: left; + margin-right: 10px; +} + +.status__avatar { + height: 48px; + left: 10px; + position: absolute; + top: 10px; + width: 48px; +} + +.muted { + .status__content p, + .status__content a { + color: lighten($color1, 26%); + } + + .status__display-name strong { + color: lighten($color1, 26%); + } + + .status__avatar { + opacity: 0.5; + } + + a.status__content__spoiler-link { + background: lighten($color1, 26%); + color: lighten($color1, 4%); + + &:hover { + background: lighten($color1, 29%); + text-decoration: none; + } + } +} + +.notification__message { + margin-left: 68px; + padding: 8px 0; + padding-bottom: 0; + cursor: default; + color: $color3; + font-size: 15px; + position: relative; + + .fa { + color: $color4; + } +} + +.notification__favourite-icon-wrapper { + left: -26px; + position: absolute; + + .star-icon { + color: #ca8f04; + } +} + +.star-icon.active { + color: #ca8f04; +} + +.notification__display-name { + color: inherit; + font-weight: 500; + text-decoration: none; + + &:hover { + color: $color5; + text-decoration: underline; + } +} + +.display-name { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.display-name__html { + font-weight: 500; +} + +.display-name__account { + font-size: 14px; +} + +.status__relative-time, +.detailed-status__datetime { + &:hover { + text-decoration: underline; + } +} + +.transparent-background, .imageloader { + background: url('../images/void.png'); +} + +.imageloader { + display: block; +} + +.navigation-bar { + padding: 10px; + display: flex; + flex-shrink: 0; + cursor: default; + color: $color3; + + strong { + color: $color5; + } + + .permalink { + text-decoration: none; + } +} + +.navigation-bar__profile { + flex: 1 1 auto; + margin-left: 8px; +} + +.navigation-bar__profile-account { + display: block; + font-weight: 500; +} + +.navigation-bar__profile-edit { + color: inherit; + text-decoration: none; +} + +.dropdown { + display: inline-block; +} + +.dropdown__content { + display: none; + position: absolute; +} + +.dropdown__sep { + border-bottom: 1px solid darken($color2, 8%); + margin: 5px 7px 6px; + padding-top: 1px; +} + +.dropdown--active .dropdown__content { + display: block; + line-height: 18px; + max-width: 311px; + right: 0; + text-align: left; + z-index: 9999; + + & > ul { + list-style: none; + background: $color2; + padding: 4px 0; + border-radius: 4px; + box-shadow: 0 0 15px rgba($color8, 0.4); + min-width: 140px; + position: relative; + } + + &.dropdown__right { + right: 0; + } + + &.dropdown__left { + & > ul { + left: -98px; + } + } + + & > ul > li > a { + font-size: 13px; + line-height: 18px; + display: block; + padding: 4px 14px; + box-sizing: border-box; + text-decoration: none; + background: $color2; + color: $color1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:focus { + outline: 0; + } + + &:hover { + background: $color4; + color: $color2; + } + } +} + +.dropdown__icon { + vertical-align: middle; +} + +.static-content { + padding: 10px; + padding-top: 20px; + color: lighten($color1, 26%); + + h1 { + font-size: 16px; + font-weight: 500; + margin-bottom: 40px; + text-align: center; + } + + p { + font-size: 13px; + margin-bottom: 20px; + } +} + +.columns-area { + display: flex; + flex: 1 1 auto; + flex-direction: row; + justify-content: flex-start; + overflow-x: auto; + position: relative; +} + +@media screen and (min-width: 360px) { + .columns-area { + padding: 10px; + } +} + +.column { + width: 330px; + position: relative; + box-sizing: border-box; + display: flex; + flex-direction: column; + + > .scrollable { + background: $color1; + } +} + +.ui { + flex: 0 0 auto; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: darken($color1, 7%); +} + +.drawer { + width: 300px; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow-y: hidden; +} + +.drawer__tab { + display: block; + flex: 1 1 auto; + padding: 15px; + padding-bottom: 13px; + color: $color3; + text-decoration: none; + text-align: center; + font-size: 16px; + border-bottom: 2px solid transparent; +} + +.column, .drawer { + flex: 1 1 100%; + overflow: hidden; +} + +@media screen and (min-width: 360px) { + .tabs-bar { + margin: 10px; + margin-bottom: 0; + } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (max-width: 1024px) { + .column, .drawer { + width: 100%; + padding: 0; + } + + .columns-area { + flex-direction: column; + } + + .search__input, .autosuggest-textarea__textarea { + font-size: 16px; + } +} + +@media screen and (min-width: 1025px) { + .columns-area { + padding: 0; + } + + .column, .drawer { + flex: 0 0 auto; + padding: 10px; + padding-left: 5px; + padding-right: 5px; + + &:first-child { + padding-left: 10px; + } + + &:last-child { + padding-right: 10px; + } + } + + .columns-area > div { + .column, .drawer { + padding-left: 5px; + padding-right: 5px; + } + } +} + +@media screen and (min-width: 1397px) { /* Width of 4 columns with margins */ + .columns-area { + margin-left: auto; + margin-right: auto; + } +} + +@media screen and (min-width: 1900px) { + .column, .drawer { + width: 400px; + border-radius: 4px; + height: 96vh; + margin-top: 2vh; + } +} + +.drawer__pager { + box-sizing: border-box; + padding: 0; + flex-grow: 1; + position: relative; + overflow: hidden; + display: flex; +} + +.drawer__inner { + position: absolute; + top: 0; + left: 0; + background: lighten($color1, 13%); + box-sizing: border-box; + padding: 0; + display: flex; + flex-direction: column; + overflow: hidden; + overflow-y: auto; + width: 100%; + height: 100%; + + &.darker { + background: $color1; + } +} + +.pseudo-drawer { + background: lighten($color1, 13%); + font-size: 13px; + text-align: left; +} + +.drawer__header { + flex: 0 0 auto; + font-size: 16px; + background: lighten($color1, 8%); + margin-bottom: 10px; + display: flex; + flex-direction: row; + + a { + transition: background 100ms ease-in; + + &:hover { + background: lighten($color1, 3%); + transition: background 200ms ease-out; + } + } +} + +.tabs-bar { + display: flex; + background: lighten($color1, 8%); + flex: 0 0 auto; + overflow-y: auto; +} + +.tabs-bar__link { + display: block; + flex: 1 1 auto; + padding: 15px 10px; + color: $color5; + text-decoration: none; + text-align: center; + font-size: 14px; + font-weight: 500; + border-bottom: 2px solid lighten($color1, 8%); + transition: all 200ms linear; + + .fa { + font-weight: 400; + font-size: 16px; + } + + &.active { + border-bottom: 2px solid $color4; + color: $color4; + } + + &:hover, &:focus, &:active { + background: lighten($color1, 14%); + transition: all 100ms linear; + } + + span { + margin-left: 5px; + display: none; + } +} + +@media screen and (min-width: 600px) { + .tabs-bar__link { + span { + display: inline; + } + } +} + +@media screen and (min-width: 1025px) { + .tabs-bar { + display: none; + } +} + +.react-autosuggest__container { + position: relative; +} + +.react-autosuggest__suggestions-container { + position: absolute; + top: 100%; + width: 100%; + z-index: 99; + box-shadow: 0 0 15px rgba($color8, 0.4); +} + +.react-autosuggest__section-title { + background: $color3; + padding: 4px 10px; + font-weight: 500; + cursor: default; + color: $color1; + text-transform: uppercase; + font-size: 11px; +} + +.react-autosuggest__suggestions-list { + background: $color2; + color: $color1; + font-size: 14px; +} + +.react-autosuggest__suggestion { + padding: 10px; + cursor: pointer; +} + +.react-autosuggest__suggestion--focused { + background: $color4; + color: $color5; +} + +.scrollable { + overflow-y: scroll; + overflow-x: hidden; + flex: 1 1 auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; + + &.optionally-scrollable { + overflow-y: auto; + } +} + +.column-back-button { + background: lighten($color1, 4%); + color: $color4; + cursor: pointer; + flex: 0 0 auto; + font-size: 16px; + padding: 15px; + z-index: 3; + + &:hover { + text-decoration: underline; + } +} + +.column-back-button__icon { + display: inline-block; + margin-right: 5px; +} + +.column-back-button--slim { + position: relative; +} + +.column-back-button--slim-button { + cursor: pointer; + flex: 0 0 auto; + font-size: 16px; + padding: 15px; + position: absolute; + right: 0; + top: -48px; +} + +.react-toggle { + display: inline-block; + position: relative; + cursor: pointer; + background-color: transparent; + border: 0; + padding: 0; + user-select: none; + -webkit-tap-highlight-color: rgba($color8, 0); + -webkit-tap-highlight-color: transparent; +} + +.react-toggle-screenreader-only { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.react-toggle--disabled { + cursor: not-allowed; + opacity: 0.5; + transition: opacity 0.25s; +} + +.react-toggle-track { + width: 50px; + height: 24px; + padding: 0; + border-radius: 30px; + background-color: $color1; + transition: all 0.2s ease; +} + +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background-color: darken($color1, 10%); +} + +.react-toggle--checked .react-toggle-track { + background-color: $color4; +} + +.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background-color: lighten($color4, 10%); +} + +.react-toggle-track-check { + position: absolute; + width: 14px; + height: 10px; + top: 0px; + bottom: 0px; + margin-top: auto; + margin-bottom: auto; + line-height: 0; + left: 8px; + opacity: 0; + transition: opacity 0.25s ease; +} + +.react-toggle--checked .react-toggle-track-check { + opacity: 1; + transition: opacity 0.25s ease; +} + +.react-toggle-track-x { + position: absolute; + width: 10px; + height: 10px; + top: 0px; + bottom: 0px; + margin-top: auto; + margin-bottom: auto; + line-height: 0; + right: 10px; + opacity: 1; + transition: opacity 0.25s ease; +} + +.react-toggle--checked .react-toggle-track-x { + opacity: 0; +} + +.react-toggle-thumb { + transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; + position: absolute; + top: 1px; + left: 1px; + width: 22px; + height: 22px; + border: 1px solid $color1; + border-radius: 50%; + background-color: darken($color5, 2%); + box-sizing: border-box; + transition: all 0.25s ease; +} + +.react-toggle--checked .react-toggle-thumb { + left: 27px; + border-color: $color4; +} + +.column-link { + background: lighten($color1, 8%); + color: $color5; + display: block; + font-size: 16px; + padding: 15px; + text-decoration: none; + + &:hover { + background: lighten($color1, 11%); + } + + &.hidden-on-mobile { + @media screen and (max-width: 1024px) { + display: none; + } + } +} + +.column-link__icon { + display: inline-block; + margin-right: 5px; +} + +.column-subheading { + background: $color1; + color: lighten($color1, 26%); + padding: 8px 20px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + cursor: default; +} + +.autosuggest-textarea, +.spoiler-input { + position: relative; +} + +.autosuggest-textarea__textarea, +.spoiler-input__input { + display: block; + box-sizing: border-box; + width: 100%; + margin: 0; + color: $color1; + padding: 10px; + font-family: inherit; + font-size: 14px; + resize: vertical; + border: 0; + outline: 0; + + &:focus { + outline: 0; + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } +} + +.spoiler-input__input { + border-radius: 4px; +} + +.autosuggest-textarea__textarea { + min-height: 100px; + background: $color5; + border-radius: 4px 4px 0 0; + padding-bottom: 0; + padding-right: 10px + 22px; + resize: none; + + @media screen and (max-width: 600px) { + height: 100px !important; // prevent auto-resize textarea + resize: vertical; + } +} + +.autosuggest-textarea__suggestions { + position: absolute; + top: 100%; + width: 100%; + z-index: 99; + box-shadow: 0 0 15px rgba($color8, 0.4); + background: $color2; + color: $color1; + font-size: 14px; +} + +.autosuggest-textarea__suggestions__item { + padding: 10px; + cursor: pointer; + + &:hover { + background: darken($color2, 10%); + } + + &.selected { + background: $color4; + color: $color5; + } +} + +.autosuggest-account { + overflow: hidden; +} + +.autosuggest-account-icon { + float: left; + margin-right: 5px; +} + +.autosuggest-status { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + strong { + font-weight: 500; + } +} + +.character-counter__wrapper { + line-height: 36px; + margin-right: 16px; + padding-top: 10px; +} + +.character-counter { + cursor: default; + font-size: 16px; +} + +.character-counter--over { + color: #ff5050; +} + +.getting-started__wrapper { + position: relative; +} + +.getting-started { + box-sizing: border-box; + padding-bottom: 235px; + background: url('../images/mastodon-getting-started.png') no-repeat 0 100%/contain local; + flex: 1 0 auto; + + p { + color: $color2; + } + + a { + color: lighten($color1, 26%); + } +} + +.setting-text { + color: $color3; + background: transparent; + border: none; + border-bottom: 2px solid $color3; + box-sizing: border-box; + display: block; + font-family: inherit; + margin-bottom: 10px; + padding: 7px 0px; + width: 100%; + + &:focus, &:active { + color: $color5; + border-bottom-color: $color4; + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } +} + +@import 'boost'; + +button.icon-button i.fa-retweet { + background-position: 0 0; + height: 19px; + transition: background-position 0.9s steps(10); + transition-duration: 0s; + vertical-align: middle; + width: 22px; + + &::before { + display: none !important; + } +} + +button.icon-button.active i.fa-retweet { + transition-duration: 0.9s; + background-position: 0 100%; +} + +.status-card { + display: flex; + cursor: pointer; + font-size: 14px; + border: 1px solid lighten($color1, 8%); + border-radius: 4px; + color: lighten($color1, 26%); + margin-top: 14px; + text-decoration: none; + overflow: hidden; + + &:hover { + background: lighten($color1, 8%); + } +} + +.status-card-video, .status-card-rich, .status-card-photo { + margin-top: 14px; + overflow: hidden; + + iframe { + width: 100%; + height: auto; + } +} + +.status-card-photo { + display: block; + text-decoration: none; + + img { + display: block; + width: 100%; + height: auto; + margin: 0; + } +} + +.status-card__title { + display: block; + font-weight: 500; + margin-bottom: 5px; + color: $color3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-card__content { + flex: 1 1 auto; + overflow: hidden; + padding: 14px 14px 14px 8px; +} + +.status-card__description { + color: $color3; +} + +.status-card__image { + flex: 0 0 100px; + background: lighten($color1, 8%); +} + +.status-card__image-image { + border-radius: 4px 0px 0px 4px; + display: block; + height: auto; + margin: 0; + width: 100%; +} + +.load-more { + display: block; + color: lighten($color1, 26%); + text-align: center; + padding: 15px; + text-decoration: none; + + &:hover { + background: lighten($color1, 2%); + } +} + +.missing-indicator { + text-align: center; + font-size: 16px; + font-weight: 500; + color: lighten($color1, 16%); + background: $color1; + cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + + & > div { + background: url('../images/mastodon-not-found.png') no-repeat center -50px; + padding-top: 210px; + width: 100%; + } +} + +.column-header { + padding: 15px; + font-size: 16px; + background: lighten($color1, 4%); + flex: 0 0 auto; + cursor: pointer; + position: relative; + z-index: 2; + outline: 0; + + &.active { + box-shadow: 0 1px 0 rgba($color4, 0.3); + } + + &.active .fa { + color: $color4; + text-shadow: 0 0 10px rgba($color4, 0.4); + } + + &.hidden-on-mobile { + @media screen and (max-width: 1024px) { + display: none; + } + } + + &:focus, &:active { + outline: 0; + } +} + +.column-header__icon { + display: inline-block; + margin-right: 5px; +} + +.loading-indicator { + color: $color2; + font-size: 16px; + font-weight: 500; + padding-top: 120px; + text-align: center; +} + +.collapsable-collapsed { + color: $color3; + background: lighten($color1, 4%); +} + +.collapsable { + color: $color5; + background: lighten($color1, 8%); + + &:hover { + color: $color5; + background: lighten($color1, 8%); + } +} + +.video-error-cover { + align-items: center; + background: $color8; + color: $color5; + cursor: pointer; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + margin-top: 8px; + position: relative; + text-align: center; + z-index: 100; +} + +.media-spoiler { + align-items: center; + background: $color8; + color: $color5; + cursor: pointer; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + position: relative; + text-align: center; + z-index: 100; +} + +.media-spoiler__warning { + display: block; + font-size: 14px; +} + +.media-spoiler__trigger { + display: block; + font-size: 11px; + font-weight: 500; +} + +.spoiler-button { + left: 4px; + position: absolute; + text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; + top: 4px; + z-index: 100; +} + +.modal-container--preloader { + background: lighten($color1, 8%); +} + +.account--panel { + background: lighten($color1, 4%); + border-top: 1px solid lighten($color1, 8%); + border-bottom: 1px solid lighten($color1, 8%); + display: flex; + flex-direction: row; + padding: 10px 0px; +} + +.account--panel__button, +.detailed-status__button { + flex: 1 1 auto; + text-align: center; +} + +.column-settings__outer { + background: lighten($color1, 8%); + padding: 15px; +} + +.column-settings__section { + color: $color3; + cursor: default; + display: block; + font-weight: 500; + margin-bottom: 10px; +} + +.modal-container__nav { + align-items: center; + background: rgba(0, 0, 0, 0.5); + box-sizing: border-box; + color: $color5; + cursor: pointer; + display: flex; + font-size: 24px; + height: 100%; + padding: 30px 15px; + position: absolute; + top: 0; +} + +.modal-container__nav--left { + left: -61px; +} + +.modal-container__nav--right { + right: -61px; +} + +.account--follows-info { + color: $color5; +} + +.setting-toggle__label { + display: block; + line-height: 24px; + vertical-align: middle; +} + +.setting-toggle { + color: $color3; + display: inline-block; + margin-bottom: 14px; + margin-left: 8px; + vertical-align: middle; +} + +.report.scrollable { + box-sizing: border-box; + display: flex; + flex-direction: column; + max-height: 100%; +} + +.report__target { + border-bottom: 1px solid lighten($color1, 4%); + color: $color2; + flex: 0 0 auto; + padding: 10px; + + strong { + display: block; + color: $color5; + font-weight: 500; + } +} + +.report__statuses { + flex: 1 1 auto; +} + +.report__textarea-wrapper { + flex: 0 0 100px; + padding: 10px; +} + +.report__textarea { + background: transparent; + box-sizing: border-box; + border: 0; + border-bottom: 2px solid $color3; + border-radius: 2px 2px 0 0; + color: $color5; + display: block; + font-family: inherit; + font-size: 14px; + margin-bottom: 10px; + outline: 0; + padding: 7px 4px; + resize: vertical; + width: 100%; + + &:active, &:focus { + border-bottom-color: $color4; + background: rgba($color8, 0.1); + } +} + +.report__submit { + margin-top: 10px; + overflow: hidden; +} + +.report__submit-button { + float: right; +} + +.empty-column-indicator { + color: lighten($color1, 20%); + background: $color1; + text-align: center; + padding: 20px; + font-size: 15px; + font-weight: 400; + cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; + + a { + color: $color4; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.status-list__unread-indicator, .notifications__unread-indicator { + position: absolute; + top: 35px; + left: 0; + right: 0; + margin: 0 auto; + width: 60%; + pointer-events: none; + height: 28px; + z-index: 1; + background: radial-gradient(ellipse, rgba($color4, 0.23) 0%, rgba($color4, 0) 60%); +} + +.emoji-dialog { + width: 245px; + height: 270px; + background: $color5; + box-sizing: border-box; + border-radius: 4px; + overflow: hidden; + position: relative; + box-shadow: 0 0 8px rgba($color8, 0.2); + + .emojione { + margin: 0; + width: 100%; + height: auto; + } + + .emoji-dialog-header { + padding: 0 10px; + + ul { + padding: 0; + margin: 0; + list-style: none; + } + + li { + display: inline-block; + box-sizing: border-box; + padding: 10px 5px; + cursor: pointer; + border-bottom: 2px solid transparent; + + .emoji { + width: 18px; + height: 18px; + } + + img, svg { + width: 18px; + height: 18px; + filter: grayscale(100%); + } + + &:hover { + img, svg { + filter: grayscale(0); + } + } + + &.active { + border-bottom-color: $color4; + + img, svg { + filter: grayscale(0); + } + } + } + } + + .emoji-row { + box-sizing: border-box; + overflow-y: hidden; + padding-left: 10px; + + .emoji { + display: inline-block; + padding: 2.5px; + border-radius: 4px; + } + } + + .emoji-category-header { + box-sizing: border-box; + overflow-y: hidden; + padding: 10px 8px 10px 16px; + display: table; + + > * { + display: table-cell; + vertical-align: middle; + } + } + + .emoji-category-title { + font-size: 12px; + text-transform: uppercase; + font-weight: 500; + color: darken($color2, 18%); + cursor: default; + } + + .emoji-category-heading-decoration { + text-align: right; + } + + .modifiers { + list-style: none; + padding: 0; + margin: 0; + vertical-align: middle; + white-space: nowrap; + margin-top: 4px; + + li { + display: inline-block; + padding: 0 2px; + + &:last-of-type { + padding-right: 0; + } + } + + .modifier { + display: inline-block; + border-radius: 10px; + width: 15px; + height: 15px; + position: relative; + cursor: pointer; + + &.active:after { + content: ""; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 10px; + border: 2px solid $color5; + top: 2px; + left: 2px; + } + } + } + + .emoji-search-wrapper { + padding: 10px; + border-bottom: 1px solid lighten($color2, 4%); + } + + .emoji-search { + font-size: 14px; + font-weight: 400; + padding: 7px 9px; + font-family: inherit; + display: block; + width: 100%; + background: rgba($color2, 0.3); + color: darken($color2, 18%); + border: 1px solid $color2; + border-radius: 4px; + } + + .emoji-categories-wrapper { + position: absolute; + top: 42px; + bottom: 0; + left: 0; + right: 0; + } + + .emoji-search-wrapper + .emoji-categories-wrapper { + top: 93px; + } + + .emoji-row .emoji { + img, svg { + transition: transform 60ms ease-in-out; + } + + &:hover { + background: lighten($color2, 3%); + + img, svg { + transform: translateZ(0) scale(1.2); + } + } + } + + .emoji { + width: 22px; + height: 22px; + cursor: pointer; + + &:focus { + outline: 0; + } + } +} + +.upload-area { + align-items: center; + background: rgba($color8, 0.8); + display: flex; + height: 100%; + justify-content: center; + left: 0; + opacity: 0; + position: absolute; + top: 0; + visibility: hidden; + width: 100%; + z-index: 2000; + + * { + pointer-events: none; + } +} + +.upload-area__drop { + width: 320px; + height: 160px; + display: flex; + box-sizing: border-box; + position: relative; + padding: 8px; +} + +.upload-area__background { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + border-radius: 4px; + background: $color1; + box-shadow: 0 0 5px rgba($color8, 0.2); +} + +.upload-area__content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: $color2; + font-size: 18px; + font-weight: 500; + border: 2px dashed lighten($color1, 26%); + border-radius: 4px; +} + +.upload-progress { + padding: 10px; + color: lighten($color1, 26%); + overflow: hidden; + display: flex; + + .fa { + font-size: 34px; + margin-right: 10px; + } + + span { + font-size: 12px; + text-transform: uppercase; + font-weight: 500; + display: block; + } +} + +.upload-progess__message { + flex: 1 1 auto; +} + +.upload-progress__backdrop { + width: 100%; + height: 6px; + border-radius: 6px; + background: lighten($color1, 26%); + position: relative; + margin-top: 5px; +} + +.upload-progress__tracker { + position: absolute; + left: 0; + top: 0; + height: 6px; + background: $color4; + border-radius: 6px; +} + +.emoji-button { + outline: 0; + + &:active, &:focus { + outline: 0 !important; + } + + img { + filter: grayscale(100%); + opacity: 0.8; + display: block; + margin: 0; + width: 22px; + height: 22px; + margin-top: 2px; + } + + &:hover, &:active, &:focus { + img { + opacity: 1; + filter: none; + } + } +} + +.dropdown--active .emoji-button img { + opacity: 1; + filter: none; +} + +.privacy-dropdown { + position: relative; +} + +.privacy-dropdown__dropdown { + display: none; + position: absolute; + left: 0; + top: 27px; + width: 230px; + background: $color5; + border-radius: 0 4px 4px 4px; + z-index: 2; + overflow: hidden; +} + +.privacy-dropdown__option { + color: $color1; + padding: 10px; + cursor: pointer; + display: flex; + + &:hover, &.active { + background: $color4; + color: $color5; + + .privacy-dropdown__option__content { + color: $color5; + + strong { + color: $color5; + } + } + } + + &.active:hover { + background: lighten($color4, 4%); + } +} + +.privacy-dropdown__option__icon { + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} + +.privacy-dropdown__option__content { + flex: 1 1 auto; + color: darken($color3, 24%); + + strong { + font-weight: 500; + display: block; + color: $color1; + } +} + +.privacy-dropdown.active { + .privacy-dropdown__value { + background: $color5; + border-radius: 4px 4px 0 0; + box-shadow: 0 -4px 4px rgba($color8, 0.1); + } + + .privacy-dropdown__dropdown { + display: block; + box-shadow: 2px 4px 6px rgba($color8, 0.1); + } +} + +.search { + position: relative; +} + +.search__input { + padding-right: 30px; + color: $color2; + outline: 0; + box-sizing: border-box; + display: block; + width: 100%; + border: none; + padding: 10px; + padding-right: 30px; + font-family: inherit; + background: $color1; + color: $color3; + font-size: 14px; + margin: 0; + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, &:focus, &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($color1, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } +} + +.search__icon { + .fa { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + display: inline-block; + opacity: 0; + transition: all 100ms linear; + font-size: 18px; + width: 18px; + height: 18px; + color: $color2; + cursor: default; + pointer-events: none; + + &.active { + pointer-events: auto; + opacity: 0.3; + } + } + + .fa-search { + transform: translateZ(0) rotate(90deg); + + &.active { + pointer-events: none; + transform: translateZ(0) rotate(0deg); + } + } + + .fa-times-circle { + top: 11px; + transform: translateZ(0) rotate(0deg); + cursor: pointer; + + &.active { + transform: translateZ(0) rotate(90deg); + } + + &:hover { + color: $color5; + } + } +} + +.search-results__header { + color: lighten($color1, 26%); + background: lighten($color1, 2%); + border-bottom: 1px solid darken($color1, 4%); + padding: 15px 10px; + font-size: 14px; + font-weight: 500; +} + +.search-results__hashtag { + display: block; + padding: 10px; + color: $color2; + text-decoration: none; + + &:hover, &:active, &:focus { + color: lighten($color2, 4%); + text-decoration: underline; + } +} + +.modal-root__overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + opacity: 0; + background: rgba($color8, 0.7); + transform: translateZ(0px); +} + +.modal-root__container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + align-content: space-around; + z-index: 9999; + opacity: 0; + pointer-events: none; + user-select: none; +} + +.modal-root__modal { + pointer-events: auto; + display: flex; + z-index: 9999; +} + +.media-modal { + max-width: 80vw; + max-height: 80vh; + position: relative; + + img, video { + max-width: 80vw; + max-height: 80vh; + } +} + +.media-modal__close { + position: absolute; + right: 4px; + top: 4px; + z-index: 100; +} + +.onboarding-modal { + background: $color2; + color: $color1; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.onboarding-modal__pager { + height: 80vh; + width: 80vw; + max-width: 520px; + max-height: 420px; + position: relative; + + & > div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 25px; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + display: flex; + opacity: 0; + user-select: text; + } +} + +@media screen and (max-width: 550px) { + .onboarding-modal { + width: 100%; + height: 100%; + border-radius: 0; + } + + .onboarding-modal__pager { + width: 100%; + height: auto; + max-width: none; + max-height: none; + flex: 1 1 auto; + } +} + +.onboarding-modal__paginator { + flex: 0 0 auto; + background: darken($color2, 8%); + display: flex; + padding: 25px; + + & > div { + min-width: 33px; + } + + a { + color: darken($color2, 34%); + text-decoration: none; + font-size: 14px; + font-weight: 500; + + &:hover, &:focus, &:active { + color: darken($color2, 38%); + } + + &.onboarding-modal__done, &.onboarding-modal__next { + color: $color4; + } + } +} + +.onboarding-modal__dots { + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: center; +} + +.onboarding-modal__dot { + width: 14px; + height: 14px; + border-radius: 14px; + background: darken($color2, 16%); + margin: 0 3px; + cursor: pointer; + + &:hover { + background: darken($color2, 18%); + } + + &.active { + cursor: default; + background: darken($color2, 24%); + } +} + +.onboarding-modal__page { + cursor: default; + line-height: 21px; + + h1 { + font-size: 18px; + font-weight: 500; + color: $color1; + margin-bottom: 20px; + } + + a { + color: $color4; + + &:hover, &:focus, &:active { + color: lighten($color4, 4%); + } + } + + p { + font-size: 16px; + color: lighten($color1, 8%); + margin-top: 10px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + strong { + font-weight: 500; + background: $color1; + color: $color2; + border-radius: 4px; + font-size: 14px; + padding: 3px 6px; + } + } +} + +.onboarding-modal__page-one { + display: flex; +} + +.onboarding-modal__page-one__elephant-friend { + background: url('../images/elephant-friend.png') no-repeat center center/contain; + width: 147px; + height: 160px; + margin-right: 10px; +} + +.onboarding-modal__page-two, +.onboarding-modal__page-three, +.onboarding-modal__page-four, +.onboarding-modal__page-five { + p { + text-align: left; + } + + .figure { + background: darken($color1, 8%); + color: $color2; + margin-bottom: 20px; + border-radius: 4px; + padding: 10px; + text-align: center; + font-size: 14px; + box-shadow: 1px 2px 6px rgba($color8, 0.3); + + .onboarding-modal__image { + border-radius: 4px; + margin-bottom: 10px; + } + + &.non-interactive { + pointer-events: none; + text-align: left; + } + } +} + +.onboarding-modal__page-four__columns { + .row { + display: flex; + margin-bottom: 20px; + + & > div { + flex: 1 1 0; + margin: 0 10px; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + p { + text-align: center; + } + } + + &:last-child { + margin-bottom: 0; + } + } + + .column-header { + color: $color5; + } +} + +.onboarding-modal__image { + border-radius: 8px; + width: 70vw; + max-width: 450px; + max-height: auto; + display: block; + margin: auto; + margin-bottom: 20px; +} + +.onboard-sliders { + display: inline-block; + max-width: 30px; + max-height: auto; + margin-left: 10px; +} + +.boost-modal, .confirmation-modal { + background: lighten($color2, 8%); + color: $color1; + border-radius: 8px; + overflow: hidden; + max-width: 90vw; + width: 480px; + position: relative; + flex-direction: column; + + .status__display-name { + display: block; + max-width: 100%; + padding-right: 25px; + } + + .status__avatar { + height: 28px; + left: 10px; + position: absolute; + top: 10px; + width: 48px; + } +} + +.boost-modal__container { + overflow-x: scroll; + padding: 10px; + + .status { + user-select: text; + border-bottom: 0; + } +} + +.boost-modal__action-bar, .confirmation-modal__action-bar { + display: flex; + background: $color2; + padding: 10px; + line-height: 36px; + + & > div { + flex: 1 1 auto; + text-align: right; + color: lighten($color1, 33%); + padding-right: 10px; + } + + .button { + flex: 0 0 auto; + } +} + +.boost-modal__status-header { + font-size: 15px; +} + +.boost-modal__status-time { + float: right; + font-size: 14px; +} + +.confirmation-modal { + max-width: 380px; +} + +.confirmation-modal__action-bar { + & > div { + text-align: left; + padding: 0 16px; + } + + a { + color: darken($color2, 34%); + text-decoration: none; + font-size: 14px; + font-weight: 500; + + &:hover, &:focus, &:active { + color: darken($color2, 38%); + } + } +} + +.confirmation-modal__container { + padding: 30px; + font-size: 16px; + text-align: center; + + strong { + font-weight: 500; + } +} + +.loading-bar { + background-color: $color4; + height: 3px; + position: absolute; + top: 0; + left: 0; +} + +.media-gallery__gifv__label { + display: block; + position: absolute; + color: $color5; + background: rgba($color8, 0.5); + bottom: 6px; + left: 6px; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: 600; + z-index: 1; + pointer-events: none; + opacity: 0.9; + transition: opacity 0.1s ease; +} + +.media-gallery__gifv { + &.autoplay { + .media-gallery__gifv__label { + display: none; + } + } + + &:hover { + .media-gallery__gifv__label { + opacity: 1; + } + } +} + +.attachment-list { + display: flex; + font-size: 14px; + border: 1px solid lighten($color1, 8%); + border-radius: 4px; + margin-top: 14px; + overflow: hidden; +} + +.attachment-list__icon { + flex: 0 0 auto; + color: lighten($color1, 26%); + padding: 8px 18px; + cursor: default; + border-right: 1px solid lighten($color1, 8%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 26px; + + .fa { + display: block; + } +} + +.attachment-list__list { + list-style: none; + padding: 4px 0; + padding-left: 8px; + display: flex; + flex-direction: column; + justify-content: center; + + li { + display: block; + padding: 4px 0; + } + + a { + text-decoration: none; + color: lighten($color1, 26%); + font-weight: 500; + + &:hover { + text-decoration: underline; + } + } +} + +/* Media Gallery */ +.media-gallery { + box-sizing: border-box; + margin-top: 8px; + overflow: hidden; + position: relative; + width: 100%; +} + +.media-gallery__item { + border: none; + box-sizing: border-box; + display: block; + float: left; + position: relative; +} + +.media-gallery__item-thumbnail { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + cursor: zoom-in; + display: block; + height: 100%; + text-decoration: none; + width: 100%; +} + +.media-gallery__gifv { + height: 100%; + overflow: hidden; + position: relative; + width: 100%; +} + +.media-gallery__item-gifv-thumbnail { + cursor: zoom-in; + height: 100%; + object-fit: cover; + position: relative; + top: 50%; + transform: translateY(-50%); + width: 100%; + z-index: 1; +} + +.media-gallery__item-thumbnail-label { + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + overflow: hidden; + position: absolute; +} +/* End Media Gallery */ + +/* Status Video Player */ +.status__video-player { + background: #000; + box-sizing: border-box; + cursor: default; /* May not be needed */ + margin-top: 8px; + overflow: hidden; + position: relative; +} + +.status__video-player-video { + height: 100%; + object-fit: cover; + position: relative; + top: 50%; + transform: translateY(-35%); + width: 100%; + z-index: 1; +} + +.status__video-player-expand, +.status__video-player-mute { + color: #fff; + opacity: 0.8; + position: absolute; + right: 4px; + text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; +} + +.status__video-player-spoiler { + color: #fff; + left: 4px; + position: absolute; + text-shadow: 0px 1px 1px #000, 1px 0px 1px #000; + top: 4px; + z-index: 100; +} + +.status__video-player-expand { + bottom: 4px; + z-index: 100; +} + +.status__video-player-mute { + top: 4px; + z-index: 5; +} + +.media-spoiler-video { + background-size: cover; + cursor: pointer; + margin-top: 8px; + position: relative; +} + +.media-spoiler-video-play-icon { + border-radius: 100px; + color: rgba(255, 255, 255, 0.8); + font-size: 36px; + left: 50%; + padding: 5px; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); +} +/* End Video Player */ diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss @@ -0,0 +1,71 @@ +.container { + width: 700px; + margin: 0 auto; + margin-top: 40px; + + @media screen and (max-width: 700px) { + width: 100%; + margin: 0; + } +} + +.mastodon-column-container { + display: flex; + height: 100%; + width: 100%; + + // 707568 - height 100% doesn't work on child of a flex item - chromium - Monorail + // https://bugs.chromium.org/p/chromium/issues/detail?id=707568 + flex: 1 1 auto; +} + +.logo-container { + max-width: 400px; + margin: 100px auto; + margin-bottom: 0; + cursor: default; + + @media screen and (max-width: 360px) { + margin: 30px auto; + } + + h1 { + display: block; + text-align: center; + color: $color5; + font-size: 48px; + font-weight: 500; + + img { + display: block; + margin: 20px auto; + width: 180px; + height: 180px; + } + + a { + color: inherit; + text-decoration: none; + outline: 0; + + img { + opacity: 0.8; + transition: opacity 0.8s ease; + } + + &:hover { + img { + opacity: 1; + transition-duration: 0.2s; + } + } + } + + small { + display: block; + font-size: 12px; + font-weight: 400; + font-family: 'Roboto Mono', monospace; + } + } +} diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss @@ -0,0 +1,11 @@ +@font-face { + font-family: 'Montserrat'; + src: local('Montserrat'); + src: url('../fonts/montserrat/Montserrat-Regular.eot'); + src: url('../fonts/montserrat/Montserrat-Regular.eot?#iefix') format('embedded-opentype'), + url('../fonts/montserrat/Montserrat-Regular.woff2') format('woff2'), + url('../fonts/montserrat/Montserrat-Regular.woff') format('woff'), + url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} diff --git a/app/javascript/styles/fonts/roboto-mono.scss b/app/javascript/styles/fonts/roboto-mono.scss @@ -0,0 +1,12 @@ +@font-face { + font-family: 'Roboto Mono'; + src: local('Roboto Mono'); + src: url('../fonts/roboto-mono/robotomono-regular-webfont.eot'); + src: url('../fonts/roboto-mono/robotomono-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto-mono/robotomono-regular-webfont.woff2') format('woff2'), + url('../fonts/roboto-mono/robotomono-regular-webfont.woff') format('woff'), + url('../fonts/roboto-mono/robotomono-regular-webfont.ttf') format('truetype'), + url('../fonts/roboto-mono/robotomono-regular-webfont.svg#roboto_monoregular') format('svg'); + font-weight: 400; + font-style: normal; +} diff --git a/app/javascript/styles/fonts/roboto.scss b/app/javascript/styles/fonts/roboto.scss @@ -0,0 +1,52 @@ +@font-face { + font-family: 'Roboto'; + src: local('Roboto'); + src: url('../fonts/roboto/roboto-italic-webfont.eot'); + src: url('../fonts/roboto/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto/roboto-italic-webfont.woff2') format('woff2'), + url('../fonts/roboto/roboto-italic-webfont.woff') format('woff'), + url('../fonts/roboto/roboto-italic-webfont.ttf') format('truetype'), + url('../fonts/roboto/roboto-italic-webfont.svg#roboto-italic-webfont') format('svg'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: local('Roboto'); + src: url('../fonts/roboto/roboto-bold-webfont.eot'); + src: local('Roboto bold'), local('roboto-bold'), + url('../fonts/roboto/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto/roboto-bold-webfont.woff2') format('woff2'), + url('../fonts/roboto/roboto-bold-webfont.woff') format('woff'), + url('../fonts/roboto/roboto-bold-webfont.ttf') format('truetype'), + url('../fonts/roboto/roboto-bold-webfont.svg#roboto-bold-webfont') format('svg'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: local('Roboto'); + src: url('../fonts/roboto/roboto-medium-webfont.eot'); + src: url('../fonts/roboto/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto/roboto-medium-webfont.woff2') format('woff2'), + url('../fonts/roboto/roboto-medium-webfont.woff') format('woff'), + url('../fonts/roboto/roboto-medium-webfont.ttf') format('truetype'), + url('../fonts/roboto/roboto-medium-webfont.svg#roboto-medium-webfont') format('svg'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: local('Roboto'); + src: url('../fonts/roboto/roboto-regular-webfont.eot'); + src: url('../fonts/roboto/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto/roboto-regular-webfont.woff2') format('woff2'), + url('../fonts/roboto/roboto-regular-webfont.woff') format('woff'), + url('../fonts/roboto/roboto-regular-webfont.ttf') format('truetype'), + url('../fonts/roboto/roboto-regular-webfont.svg#roboto-regular-webfont') format('svg'); + font-weight: normal; + font-style: normal; +} diff --git a/app/javascript/styles/footer.scss b/app/javascript/styles/footer.scss @@ -0,0 +1,29 @@ +.footer { + text-align: center; + margin-top: 30px; + font-size: 12px; + color: darken($color2, 25%); + + .domain { + font-weight: 500; + + a { + color: inherit; + text-decoration: none; + } + } + + .powered-by, .single-user-login { + font-weight: 400; + + a { + color: inherit; + text-decoration: underline; + font-weight: 500; + + &:hover { + text-decoration: none; + } + } + } +} diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss @@ -0,0 +1,335 @@ +code { + font-family: 'Roboto Mono', monospace; + font-weight: 400; +} + +.form-container { + max-width: 400px; + padding: 20px; + margin: 0 auto; +} + +.simple_form { + .input { + margin-bottom: 15px; + } + + span.hint { + display: block; + color: $color3; + font-size: 12px; + margin-top: 4px; + } + + p.hint { + margin-bottom: 15px; + } + + strong { + font-weight: 500; + } + + .label_input { + display: flex; + + label { + flex: 0 0 auto; + width: 100px; + } + + input { + flex: 1 1 auto; + } + } + + .input.file, .input.select, .input.radio_buttons { + padding: 15px 0; + margin-bottom: 0; + + label { + font-family: inherit; + font-size: 16px; + color: $color5; + display: block; + padding-top: 5px; + } + } + + .fields-group { + margin-bottom: 25px; + } + + .input.radio_buttons .radio label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: white; + display: block; + width: auto; + } + + .input.boolean { + margin-bottom: 5px; + + label { + font-family: inherit; + font-size: 14px; + color: white; + display: block; + width: auto; + } + + label.checkbox { + position: relative; + padding-left: 25px; + flex: 1 1 auto; + } + + input[type=checkbox] { + position: absolute; + left: 0; + top: 1px; + margin: 0; + } + + .hint { + padding-left: 25px; + margin-left: 0; + } + } + + input[type=text], input[type=number], input[type=email], input[type=password], textarea { + background: transparent; + box-sizing: border-box; + border: 0; + border-bottom: 2px solid $color3; + border-radius: 2px 2px 0 0; + padding: 7px 4px; + font-size: 16px; + color: $color5; + display: block; + width: 100%; + outline: 0; + font-family: inherit; + resize: vertical; + + &:invalid { + box-shadow: none; + } + + &:focus:invalid { + border-bottom-color: $color6; + } + + &:required:valid { + border-bottom-color: $color7; + } + + &:active, &:focus { + border-bottom-color: $color4; + background: rgba($color8, 0.1); + } + } + + .input.field_with_errors { + label { + color: $color6; + } + + input[type=text], input[type=email], input[type=password] { + border-bottom-color: $color6; + } + + .error { + display: block; + font-weight: 500; + color: $color6; + margin-top: 4px; + } + } + + .actions { + margin-top: 30px; + } + + button, .block-button { + display: block; + width: 100%; + border: 0; + border-radius: 4px; + background: $color4; + color: $color5; + font-size: 18px; + padding: 10px; + text-transform: uppercase; + text-decoration: none; + text-align: center; + box-sizing: border-box; + cursor: pointer; + font-weight: 500; + outline: 0; + margin-bottom: 10px; + + &:hover { + background-color: lighten($color4, 5%); + } + + &:active, &:focus { + position: relative; + top: 1px; + background-color: darken($color4, 5%); + } + + &.negative { + background: $color6; + + &:hover { + background-color: lighten($color6, 5%); + } + + &:active, &:focus { + background-color: darken($color6, 5%); + } + } + } + + select { + font-size: 16px; + } +} + +.flash-message { + background: $color1; + color: $color3; + border-radius: 4px; + padding: 15px 10px; + margin-bottom: 30px; + box-shadow: 0 0 5px rgba($color8, 0.2); + text-align: center; + + strong { + font-weight: 500; + } +} + +.form-footer { + margin-top: 30px; + text-align: center; + + a { + color: $color5; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.oauth-prompt, .follow-prompt { + margin-bottom: 30px; + text-align: center; + color: $color3; + + h2 { + font-size: 16px; + margin-bottom: 30px; + } + + strong { + color: $color2; + font-weight: 500; + } +} + +.qr-wrapper { + display: flex; +} + +.qr-code { + flex: 0 0 auto; + background: #fff; + padding: 4px; + margin-bottom: 20px; + box-shadow: 0 0 15px rgba($color8, 0.2); + display: inline-block; + + svg { + display: block; + margin: 0; + } +} + +.qr-alternative { + margin-left: 10px; + color: $color3; + + samp { + display: block; + font-size: 14px; + } +} + +.table-form { + p { + max-width: 400px; + margin-bottom: 15px; + + strong { + font-weight: 500; + } + } + + .warning { + max-width: 400px; + box-sizing: border-box; + background: rgba($color6, 0.5); + color: $color5; + text-shadow: 1px 1px 0 rgba($color8, 0.3); + box-shadow: 0 2px 6px rgba($color8, 0.4); + border-radius: 4px; + padding: 10px; + margin-bottom: 15px; + + a { + color: $color5; + text-decoration: underline; + + &:hover, &:focus, &:active { + text-decoration: none; + } + } + + strong { + font-weight: 600; + display: block; + margin-bottom: 5px; + + .fa { + font-weight: 400; + } + } + } +} + +.action-pagination { + display: flex; + align-items: center; + + .actions, .pagination { + flex: 1 1 auto; + } + + .actions { + padding: 30px 0; + padding-right: 20px; + flex: 0 0 auto; + } +} + +.user_allowed_languages { + li { + float: left; + width: 50%; + } +} diff --git a/app/javascript/styles/landing_strip.scss b/app/javascript/styles/landing_strip.scss @@ -0,0 +1,17 @@ +.landing-strip { + background: rgba(darken($color1, 7%), 0.8); + color: $color3; + font-weight: 400; + padding: 14px; + border-radius: 4px; + margin-bottom: 20px; + + strong, a { + font-weight: 500; + } + + a { + color: inherit; + text-decoration: underline; + } +} diff --git a/app/javascript/styles/lists.scss b/app/javascript/styles/lists.scss @@ -0,0 +1,20 @@ +.no-list { + list-style: none; + + li { + display: inline-block; + margin: 0 5px; + } +} + +.recovery-codes { + list-style: none; + margin: 0 auto; + text-align: center; + + li { + font-size: 125%; + line-height: 1.5; + letter-spacing: 1px; + } +} diff --git a/app/javascript/styles/reset.scss b/app/javascript/styles/reset.scss @@ -0,0 +1,91 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: lighten($color1, 4%); + border: 0px none $color5; + border-radius: 50px; +} + +::-webkit-scrollbar-thumb:hover { + background: lighten($color1, 6%); +} + +::-webkit-scrollbar-thumb:active { + background: lighten($color1, 4%); +} + +::-webkit-scrollbar-track { + border: 0px none $color5; + border-radius: 0; + background: rgba($color8, 0.1); +} + +::-webkit-scrollbar-track:hover { + background: $color1; +} + +::-webkit-scrollbar-track:active { + background: $color1; +} + +::-webkit-scrollbar-corner { + background: transparent; +} diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss @@ -0,0 +1,136 @@ +body.rtl { + direction: rtl; + + .column-link__icon, .column-header__icon { + margin-right: 0; + margin-left: 5px; + } + + .character-counter__wrapper { + margin-right: 0; + margin-left: 16px; + } + + .navigation-bar__profile { + margin-left: 0; + margin-right: 8px; + } + + .search__input { + padding-right: 10px; + padding-left: 30px; + } + + .search__icon .fa { + right: auto; + left: 10px; + } + + .column-icon-clear { + right: auto; + left: 48px; + } + + .column-icon { + right: auto; + left: 5px; + } + + .setting-toggle { + margin-left: 0; + margin-right: 8px; + } + + .status__avatar { + left: auto; + right: 10px; + } + + .status { + padding-left: 10px; + padding-right: 68px; + } + + .status__info .status__display-name { + padding-left: 25px; + padding-right: 0; + } + + .column-back-button--slim-button { + right: auto; + left: 0; + } + + .status__info-time { + float: left; + } + + .status__action-bar-button-wrapper { + float: right; + margin-right: 0; + margin-left: 18px; + } + + .status__action-bar-dropdown { + float: right; + } + + .privacy-dropdown__dropdown { + left: auto; + right: 0; + } + + .dropdown--active .dropdown__content { + text-align: right; + } + + .dropdown--active .dropdown__content::before { + left: auto; + right: 8px; + } + + .dropdown--active .dropdown__content > ul { + left: auto; + right: -10px; + } + + .privacy-dropdown__option__icon { + margin-left: 10px; + margin-right: 0; + } + + .detailed-status__display-avatar { + margin-right: 0; + margin-left: 10px; + float: right; + } + + .detailed-status__favorites, .detailed-status__reblogs { + margin-left: 0; + margin-right: 6px; + } + + @media screen and (min-width: 1025px) { + .column, .drawer { + padding-left: 5px; + padding-right: 5px; + + &:first-child { + padding-left: 5px; + padding-right: 10px; + } + + &:last-child { + padding-right: 0px; + padding-left: 10px; + } + } + + .columns-area > div { + .column, .drawer { + padding-left: 5px; + padding-right: 5px; + } + } + } +} diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss @@ -0,0 +1,372 @@ +.activity-stream { + clear: both; + box-shadow: 0 0 15px rgba($color8, 0.2); + + .entry { + background: $color5; + + .detailed-status.light, .status.light { + border-bottom: 1px solid $color2; + } + + &:last-child { + &, .detailed-status.light, .status.light { + border-bottom: 0; + border-radius: 0 0 4px 4px; + } + } + + &:first-child { + &, .detailed-status.light, .status.light { + border-radius: 4px 4px 0 0; + } + + &:last-child { + &, .detailed-status.light, .status.light { + border-radius: 4px; + } + } + } + } + + .status.light { + padding: 14px 14px 14px (48px + 14px*2); + position: relative; + min-height: 48px; + cursor: default; + + .status__header { + font-size: 15px; + + .status__meta { + float: right; + font-size: 14px; + + .status__relative-time { + color: $color4; + } + } + } + + .status__display-name { + display: block; + max-width: 100%; + padding-right: 25px; + color: $color1; + } + + .status__avatar { + position: absolute; + left: 14px; + top: 14px; + width: 48px; + height: 48px; + + & > div { + width: 48px; + height: 48px; + } + + img { + display: block; + border-radius: 4px; + } + } + + .display-name { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + strong { + font-weight: 500; + color: $color1; + } + + span { + font-size: 14px; + color: $color4; + } + } + + .status__content { + color: $color1; + + a { + color: $color4; + } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } + } + + .status__attachments { + margin-top: 8px; + overflow: hidden; + width: 100%; + box-sizing: border-box; + position: relative; + + .status__attachments__inner { + display: flex; + height: 214px; + } + } + } + + .detailed-status.light { + padding: 14px; + background: $color5; + cursor: default; + + .detailed-status__display-name { + display: block; + overflow: hidden; + margin-bottom: 15px; + + & > div { + float: left; + margin-right: 10px; + } + + .display-name { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + strong { + font-weight: 500; + color: $color1; + } + + span { + font-size: 14px; + color: $color3; + } + } + } + + .avatar { + width: 48px; + height: 48px; + + img { + display: block; + border-radius: 4px; + } + } + + .status__content { + color: $color1; + + a { + color: $color4; + } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } + } + + .detailed-status__meta { + margin-top: 15px; + color: $color3; + font-size: 14px; + line-height: 18px; + + a { + color: inherit; + } + + span > span { + font-weight: 500; + font-size: 12px; + margin-left: 6px; + display: inline-block; + } + } + + .detailed-status__attachments { + margin-top: 8px; + overflow: hidden; + width: 100%; + box-sizing: border-box; + position: relative; + + .status__attachments__inner { + display: flex; + height: 360px; + } + } + + .video-player { + margin-top: 8px; + height: 300px; + overflow: hidden; + position: relative; + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); + } + } + } + + .media-item, .video-item { + box-sizing: border-box; + position: relative; + left: auto; + top: auto; + right: auto; + bottom: auto; + float: left; + border: medium none; + display: block; + flex: 1 1 auto; + height: 100%; + margin-right: 2px; + + &:last-child { + margin-right: 0; + } + + a { + display: block; + width: 100%; + height: 100%; + background: no-repeat scroll center center / cover; + text-decoration: none; + cursor: zoom-in; + } + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); + } + } + + .video-item { + a { + cursor: pointer; + } + + .video-item__play { + position: absolute; + top: 50%; + left: 50%; + font-size: 36px; + transform: translate(-50%, -50%); + padding: 5px; + border-radius: 100px; + color: rgba($color5, 0.8); + z-index: 1; + } + } + + .media-spoiler { + background: $color3; + width: 100%; + height: 100%; + cursor: pointer; + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + transition: all 100ms linear; + z-index: 2; + + &:hover { + background: darken($color3, 5%); + } + + span { + display: block; + + &:first-child { + font-size: 14px; + } + + &:last-child { + font-size: 11px; + font-weight: 500; + } + } + } + + .pre-header { + padding: 14px 0px; + padding-left: (48px + 14px*2); + padding-bottom: 0; + margin-bottom: -4px; + color: $color3; + font-size: 14px; + position: relative; + + .pre-header__icon { + position: absolute; + left: (48px + 14px*2 - 30px); + } + + .status__display-name.muted strong { + color: $color3; + } + } + + .open-in-web-link { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.embed { + .activity-stream { + border-radius: 4px; + box-shadow: none; + + .entry { + &:last-child { + border-radius: 0 0 4px 4px; + } + + &:first-child { + border-radius: 4px 4px 0 0; + + &:last-child { + border-radius: 4px; + } + } + } + } +} diff --git a/app/javascript/styles/tables.scss b/app/javascript/styles/tables.scss @@ -0,0 +1,65 @@ +.table { + width: 100%; + max-width: 100%; + border-spacing: 0; + border-collapse: collapse; + margin-bottom: 20px; + + th, td { + padding: 8px; + line-height: 18px; + vertical-align: top; + border-top: 1px solid $color1; + text-align: left; + } + + & > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid $color1; + border-top: 0; + font-weight: 500; + } + + & > tbody > tr > th { + font-weight: 500; + } + + & > tbody > tr:nth-child(odd) > td, & > tbody > tr:nth-child(odd) > th { + background: $color1; + } + + a { + color: $color4; + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + + strong { + font-weight: 500; + } +} + +samp { + font-family: 'Roboto Mono', monospace; +} + +a.table-action-link { + text-decoration: none; + display: inline-block; + margin-right: 5px; + padding: 0 10px; + color: rgba($color5, 0.7); + font-weight: 500; + + &:hover { + color: $color5; + } + + i.fa { + font-weight: 400; + margin-right: 5px; + } +} diff --git a/app/javascript/styles/variables.scss b/app/javascript/styles/variables.scss @@ -0,0 +1,8 @@ +$color1: #282c37 !default; // darkest +$color2: #d9e1e8 !default; // lightest +$color3: #9baec8 !default; // lighter +$color4: #2b90d9 !default; // vibrant +$color5: #ffffff !default; // white +$color6: #df405a !default; // error red +$color7: #79bd9a !default; // succ green +$color8: #000000 !default; // black diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - = javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :page_title do = site_hostname @@ -10,20 +10,20 @@ %meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:title', content: site_hostname }/ %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/ - %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/ + %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg') }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ %meta{ property: 'twitter:card', content: 'summary' }/ .wrapper %h1 - = image_tag 'logo.png' + = image_tag asset_pack_path('logo.png') = Setting.site_title %p= t('about.about_mastodon').html_safe .screenshot-with-signup - .mascot= image_tag 'fluffy-elephant-friend.png' + .mascot= image_tag asset_pack_path('fluffy-elephant-friend.png') - if @instance_presenter.open_registrations = render 'registration' diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml @@ -1,6 +1,6 @@ - content_for :header_tags do %script#initial-state{:type => 'application/json'}!= json_escape(render(file: 'home/initial_state', formats: :json)) - = javascript_include_tag 'application', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' -= react_component 'Mastodon', default_props, class: 'app-holder', prerender: false +.app-holder#mastodon{ data: { props: Oj.dump(default_props) }} diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml @@ -1,12 +1,12 @@ - content_for :header_tags do - = javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :content do .admin-wrapper .sidebar-wrapper .sidebar = link_to root_path do - = image_tag 'logo.png', class: 'logo' + = image_tag asset_pack_path('logo.png'), class: 'logo' = render_navigation .content-wrapper diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml @@ -19,7 +19,9 @@ = ' - ' = title - = stylesheet_link_tag stylesheet_for_layout, media: 'all' + = stylesheet_pack_tag 'vendor', media: 'all' + = stylesheet_pack_tag 'application', media: 'all' + = javascript_pack_tag 'vendor', integrity: true, crossorigin: 'anonymous' = csrf_meta_tags = yield :header_tags diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml @@ -1,12 +1,12 @@ - content_for :header_tags do - = javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :content do .container .logo-container %h1 = link_to root_path do - = image_tag 'logo.png' + = image_tag asset_pack_path('logo.png') .form-container = render 'flashes' diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml @@ -3,6 +3,6 @@ %head %meta{:charset => 'utf-8'}/ = stylesheet_link_tag 'application', media: 'all' - = javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' %body.embed = yield diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - = javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :content do .container= yield diff --git a/bin/webpack b/bin/webpack @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +$stdout.sync = true + +require "shellwords" +require "yaml" + +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] + +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] + +APP_PATH = File.expand_path("../", __dir__) +CONFIG_PATH = File.join(APP_PATH, "config/webpack/paths.yml") + +begin + paths = YAML.load(File.read(CONFIG_PATH))[NODE_ENV] + + NODE_MODULES_PATH = File.join(APP_PATH.shellescape, paths["node_modules"]) + WEBPACK_CONFIG_PATH = File.join(APP_PATH.shellescape, paths["config"]) +rescue Errno::ENOENT, NoMethodError + puts "Configuration not found in config/webpack/paths.yml" + puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! +end + +WEBPACK_BIN = "#{NODE_MODULES_PATH}/.bin/webpack" +WEBPACK_CONFIG = "#{WEBPACK_CONFIG_PATH}/#{NODE_ENV}.js" + +Dir.chdir(APP_PATH) do + exec "NODE_PATH=#{NODE_MODULES_PATH} #{WEBPACK_BIN} --config #{WEBPACK_CONFIG}" \ + " #{ARGV.join(" ")}" +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +$stdout.sync = true + +require "shellwords" +require "yaml" + +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] + +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] + +APP_PATH = File.expand_path("../", __dir__) +CONFIG_PATH = File.join(APP_PATH, "config/webpack/paths.yml") + +begin + paths = YAML.load(File.read(CONFIG_PATH))[NODE_ENV] + + NODE_MODULES_PATH = File.join(APP_PATH.shellescape, paths["node_modules"]) + WEBPACK_CONFIG_PATH = File.join(APP_PATH.shellescape, paths["config"]) + + WEBPACK_BIN = "#{NODE_MODULES_PATH}/.bin/webpack-dev-server" + DEV_SERVER_CONFIG = "#{WEBPACK_CONFIG_PATH}/development.server.js" +rescue Errno::ENOENT, NoMethodError + puts "Configuration not found in config/webpacker/paths.yml." + puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! +end + +Dir.chdir(APP_PATH) do + exec "NODE_PATH=#{NODE_MODULES_PATH} #{WEBPACK_BIN} --progress --color " \ + "--config #{DEV_SERVER_CONFIG} #{ARGV.join(" ")}" +end diff --git a/bin/yarn b/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +VENDOR_PATH = File.expand_path('..', __dir__) +Dir.chdir(VENDOR_PATH) do + begin + exec "yarnpkg #{ARGV.join(" ")}" + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config/application.rb b/config/application.rb @@ -74,10 +74,6 @@ module Mastodon config.middleware.use Rack::Attack config.middleware.use Rack::Deflater - # babel config can be found in .babelrc - config.browserify_rails.commandline_options = '--transform babelify --extension=".jsx"' - config.browserify_rails.evaluate_node_modules = true - config.to_prepare do Doorkeeper::AuthorizationsController.layout 'public' Doorkeeper::AuthorizedApplicationsController.layout 'admin' diff --git a/config/environments/development.rb b/config/environments/development.rb @@ -75,8 +75,6 @@ Rails.application.configure do Bullet.add_whitelist type: :n_plus_one_query, class_name: 'User', association: :account end - - config.react.variant = :development end require 'sidekiq/testing' diff --git a/config/environments/production.rb b/config/environments/production.rb @@ -106,8 +106,6 @@ Rails.application.configure do config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym - config.react.variant = :production - config.to_prepare do StatsD.backend = StatsD::Instrument::Backends::NullBackend.new if ENV['STATSD_ADDR'].blank? end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb @@ -8,6 +8,6 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w(application_public.js custom.css) +# Rails.application.config.assets.precompile += %w(application_public.js custom.css) Rails.application.config.assets.initialize_on_precompile = true diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js @@ -0,0 +1,26 @@ +// Common configuration for webpacker loaded from config/webpack/paths.yml + +const { join, resolve } = require('path') +const { env } = require('process') +const { safeLoad } = require('js-yaml') +const { readFileSync } = require('fs') + +const configPath = resolve('config', 'webpack') +const loadersDir = join(__dirname, 'loaders') +const paths = safeLoad(readFileSync(join(configPath, 'paths.yml'), 'utf8'))[env.NODE_ENV] +const devServer = safeLoad(readFileSync(join(configPath, 'development.server.yml'), 'utf8'))[env.NODE_ENV] + +// Compute public path based on environment and CDN_HOST in production +const ifHasCDN = env.CDN_HOST !== undefined && env.NODE_ENV === 'production' +const devServerUrl = `http://${devServer.host}:${devServer.port}/${paths.entry}/` +const publicUrl = ifHasCDN ? `${env.CDN_HOST}/${paths.entry}/` : `/${paths.entry}/` +const publicPath = env.NODE_ENV !== 'production' ? devServerUrl : publicUrl + +module.exports = { + devServer, + env, + paths, + loadersDir, + publicUrl, + publicPath +} diff --git a/config/webpack/development.js b/config/webpack/development.js @@ -0,0 +1,16 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +const merge = require('webpack-merge') +const sharedConfig = require('./shared.js') + +module.exports = merge(sharedConfig, { + devtool: 'sourcemap', + + stats: { + errorDetails: true + }, + + output: { + pathinfo: true + } +}) diff --git a/config/webpack/development.server.js b/config/webpack/development.server.js @@ -0,0 +1,18 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +const { resolve } = require('path') +const merge = require('webpack-merge') +const devConfig = require('./development.js') +const { devServer, publicPath, paths } = require('./configuration.js') + +module.exports = merge(devConfig, { + devServer: { + host: devServer.host, + port: devServer.port, + headers: { "Access-Control-Allow-Origin": "*" }, + compress: true, + historyApiFallback: true, + contentBase: resolve(paths.output, paths.entry), + publicPath + } +}) diff --git a/config/webpack/development.server.yml b/config/webpack/development.server.yml @@ -0,0 +1,17 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + enabled: true + host: localhost + port: 8080 + +development: + <<: *default + +test: + <<: *default + enabled: false + +production: + <<: *default + enabled: false diff --git a/config/webpack/loaders/assets.js b/config/webpack/loaders/assets.js @@ -0,0 +1,12 @@ +const { env, publicPath } = require('../configuration.js') + +module.exports = { + test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i, + use: [{ + loader: 'file-loader', + options: { + publicPath, + name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]' + } + }] +} diff --git a/config/webpack/loaders/babel.js b/config/webpack/loaders/babel.js @@ -0,0 +1,5 @@ +module.exports = { + test: /\.js(\.erb)?$/, + exclude: /node_modules/, + loader: 'babel-loader' +} diff --git a/config/webpack/loaders/coffee.js b/config/webpack/loaders/coffee.js @@ -0,0 +1,4 @@ +module.exports = { + test: /\.coffee(\.erb)?$/, + loader: 'coffee-loader' +} diff --git a/config/webpack/loaders/erb.js b/config/webpack/loaders/erb.js @@ -0,0 +1,9 @@ +module.exports = { + test: /\.erb$/, + enforce: 'pre', + exclude: /node_modules/, + loader: 'rails-erb-loader', + options: { + runner: 'bin/rails runner' + } +} diff --git a/config/webpack/loaders/sass.js b/config/webpack/loaders/sass.js @@ -0,0 +1,14 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const { env } = require('../configuration.js') + +module.exports = { + test: /\.(scss|sass|css)$/i, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: [ + { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } }, + 'postcss-loader', + 'sass-loader' + ] + }) +} diff --git a/config/webpack/paths.yml b/config/webpack/paths.yml @@ -0,0 +1,33 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + config: config/webpack + entry: packs + output: public + manifest: manifest.json + node_modules: node_modules + source: app/javascript + extensions: + - .coffee + - .js + - .jsx + - .ts + - .vue + - .sass + - .scss + - .css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + +test: + <<: *default + manifest: manifest-test.json + +production: + <<: *default diff --git a/config/webpack/production.js b/config/webpack/production.js @@ -0,0 +1,44 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +/* eslint global-require: 0 */ + +const webpack = require('webpack') +const merge = require('webpack-merge') +const CompressionPlugin = require('compression-webpack-plugin') +const sharedConfig = require('./shared.js') + +module.exports = merge(sharedConfig, { + output: { filename: '[name]-[chunkhash].js' }, + + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + compress: { + unused: true, + evaluate: true, + booleans: true, + drop_debugger: true, + dead_code: true, + pure_getters: true, + negate_iife: true, + conditionals: true, + loops: true, + cascade: true, + keep_fargs: false, + warnings: true + }, + + mangle: false, + + output: { + comments: false + }, + + sourceMap: false + }), + new CompressionPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: /\.(js|css|svg|eot|ttf|woff|woff2)$/ + }) + ] +}) diff --git a/config/webpack/shared.js b/config/webpack/shared.js @@ -0,0 +1,59 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +/* eslint global-require: 0 */ +/* eslint import/no-dynamic-require: 0 */ + +const webpack = require('webpack') +const { basename, dirname, join, relative, resolve } = require('path') +const { sync } = require('glob') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const ManifestPlugin = require('webpack-manifest-plugin') +const extname = require('path-complete-extname') +const { env, paths, publicPath, loadersDir } = require('./configuration.js') + +const extensionGlob = `**/*{${paths.extensions.join(',')}}*` +const packPaths = sync(join(paths.source, paths.entry, extensionGlob)) + +module.exports = { + entry: packPaths.reduce( + (map, entry) => { + const localMap = map + const namespace = relative(join(paths.source, paths.entry), dirname(entry)) + localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry) + return localMap + }, {} + ), + + output: { + filename: '[name].js', + chunkFilename: '[name]-[chunkhash].js', + path: resolve(paths.output, paths.entry), + publicPath + }, + + module: { + rules: sync(join(loadersDir, '*.js')).map(loader => require(loader)) + }, + + plugins: [ + new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))), + new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'), + new ManifestPlugin({ fileName: paths.manifest, publicPath, writeToFileEmit: true }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: ({ resource }) => /node_modules/.test(resource) + }) + ], + + resolve: { + extensions: paths.extensions, + modules: [ + resolve(paths.source), + resolve(paths.node_modules) + ] + }, + + resolveLoader: { + modules: [paths.node_modules] + } +} diff --git a/config/webpack/test.js b/config/webpack/test.js @@ -0,0 +1,6 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +const merge = require('webpack-merge') +const sharedConfig = require('./shared.js') + +module.exports = merge(sharedConfig, {}) diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js @@ -0,0 +1,34 @@ +const manageTranslations = require('react-intl-translations-manager').default; + +manageTranslations({ + messagesDirectory: 'build/messages', + translationsDirectory: 'app/javascript/mastodon/locales/', + detectDuplicateIds: false, + singleMessagesFile: true, + languages: [ + 'ar', + 'en', + 'de', + 'es', + 'fa', + 'hr', + 'hu', + 'io', + 'it', + 'fr', + 'nl', + 'no', + 'oc', + 'pt', + 'pt-BR', + 'uk', + 'fi', + 'eo', + 'ru', + 'ja', + 'zh-HK', + 'zh-CN', + 'bg', + 'id', + ], +}) diff --git a/docker-compose.yml b/docker-compose.yml @@ -15,10 +15,14 @@ services: # volumes: # - ./redis:/data - web: - restart: always + app: build: . image: gargron/mastodon + + web: + extends: + service: app + restart: always env_file: .env.production command: bundle exec rails s -p 3000 -b '0.0.0.0' ports: @@ -28,12 +32,13 @@ services: - redis volumes: - ./public/assets:/mastodon/public/assets + - ./public/packs:/mastodon/public/packs - ./public/system:/mastodon/public/system streaming: + extends: + service: app restart: always - build: . - image: gargron/mastodon env_file: .env.production command: npm run start ports: @@ -43,9 +48,9 @@ services: - redis sidekiq: + extends: + service: app restart: always - build: . - image: gargron/mastodon env_file: .env.production command: bundle exec sidekiq -q default -q mailers -q pull -q push depends_on: diff --git a/package.json b/package.json @@ -3,10 +3,11 @@ "license": "AGPL-3.0", "scripts": { "postversion": "git push --tags", + "manage:translations": "node ./config/webpack/translationRunner.js", "start": "babel-node ./streaming/index.js --presets es2015,stage-2", "storybook": "start-storybook -p 9001 -c storybook", "test": "npm run test:lint && npm run test:mocha", - "test:lint": "eslint -c .eslintrc.json --ext=js --ext=jsx app/assets/javascripts/", + "test:lint": "eslint -c .eslintrc.json --ext=js --ext=jsx app/javascript/", "test:mocha": "mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.jsx" }, "repository": { @@ -14,39 +15,55 @@ "url": "https://github.com/tootsuite/mastodon.git" }, "dependencies": { - "@kadira/storybook": "^2.35.3", + "autoprefixer": "^6.7.7", "axios": "^0.15.3", "babel-cli": "^6.23.0", + "babel-core": "^6.24.1", + "babel-loader": "7.x", + "babel-plugin-react-intl": "^2.3.1", "babel-plugin-react-transform": "^2.0.2", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-constant-elements": "^6.23.0", + "babel-plugin-transform-react-inline-elements": "^6.22.0", + "babel-plugin-transform-react-jsx-self": "^6.22.0", + "babel-plugin-transform-react-jsx-source": "^6.22.0", + "babel-plugin-transform-react-pure-class-to-function": "^1.0.1", + "babel-plugin-transform-react-remove-prop-types": "^0.4.3", + "babel-preset-env": "^1.4.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.11.1", "babel-preset-stage-2": "^6.22.0", - "babelify": "^7.3.0", - "browserify": "^14.1.0", - "browserify-incremental": "^3.1.1", "bufferutil": "^2.0.1", - "chai": "^3.5.0", - "chai-enzyme": "^0.6.1", - "css-loader": "^0.26.2", + "coffee-loader": "^0.7.3", + "coffee-script": "^1.12.5", + "compression-webpack-plugin": "^0.4.0", + "css-loader": "^0.28.0", "dotenv": "^4.0.0", "emojione": "^2.2.7", "emojione-picker": "^2.1.2", - "enzyme": "^2.8.2", "es6-promise": "^3.2.1", "escape-html": "^1.0.3", "eventsource": "^0.2.1", "express": "^4.14.1", + "extract-text-webpack-plugin": "^2.1.0", + "file-loader": "^0.11.1", + "font-awesome": "^4.7.0", + "glob": "^7.1.1", "http-link-header": "^0.8.0", "immutable": "^3.8.1", "intl": "^1.2.5", - "jsdom": "^9.11.0", - "mocha": "^3.2.0", + "jquery-ujs": "^1.2.2", + "js-yaml": "^3.8.3", "node-sass": "^4.5.2", "npmlog": "^4.0.2", + "path-complete-extname": "^0.1.0", "pg": "^6.1.2", + "postcss-loader": "^1.3.3", + "postcss-smart-import": "^0.6.12", + "precss": "^1.4.0", "prop-types": "^15.5.8", + "rails-erb-loader": "^5.0.0", "react": "^15.5.4", "react-addons-perf": "^15.4.2", "react-addons-shallow-compare": "^15.5.2", @@ -55,6 +72,7 @@ "react-dom": "^15.5.4", "react-imageloader": "^2.1.0", "react-immutable-proptypes": "^2.1.0", + "react-immutable-pure-component": "^0.0.4", "react-intl": "^2.1.5", "react-motion": "^0.4.5", "react-notification": "^6.6.0", @@ -72,21 +90,31 @@ "redux-immutable": "^3.1.0", "redux-thunk": "^2.2.0", "reselect": "^2.5.4", - "sass-loader": "^6.0.2", + "sass-loader": "^6.0.3", "sinon": "^1.17.6", "stringz": "^0.1.2", - "style-loader": "^0.13.2", + "style-loader": "^0.16.1", "utf-8-validate": "^3.0.1", "uuid": "^3.0.1", - "webpack": "^2.2.1", + "webpack": "^2.4.1", + "webpack-manifest-plugin": "^1.1.0", + "webpack-merge": "^4.1.0", "websocket.js": "^0.1.7", "ws": "^2.1.0" }, "devDependencies": { + "@kadira/storybook": "^2.35.3", "babel-eslint": "^7.2.2", + "chai": "^3.5.0", + "chai-enzyme": "^0.6.1", + "enzyme": "^2.8.2", "eslint": "^3.19.0", "eslint-plugin-jsx-a11y": "^4.0.0", - "eslint-plugin-react": "^6.10.3" + "eslint-plugin-react": "^6.10.3", + "jsdom": "^9.11.0", + "mocha": "^3.2.0", + "react-intl-translations-manager": "^5.0.0", + "webpack-dev-server": "^2.4.5" }, "optionalDependencies": { "fsevents": "*" diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -27,14 +27,11 @@ RSpec.describe Api::V1::AccountsController, type: :controller do describe 'PATCH #update_credentials' do describe 'with valid data' do before do - avatar = File.read(Rails.root.join('app', 'assets', 'images', 'logo.png')) - header = File.read(Rails.root.join('app', 'assets', 'images', 'mastodon-getting-started.png')) - patch :update_credentials, params: { display_name: "Alice Isn't Dead", note: "Hi!\n\nToot toot!", - avatar: "data:image/png;base64,#{Base64.encode64(avatar)}", - header: "data:image/png;base64,#{Base64.encode64(header)}", + avatar: fixture_file_upload('files/avatar.gif', 'image/gif'), + header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'), } end diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb @@ -11,6 +11,6 @@ feature "Log in" do fill_in "user_password", with: password click_on "Log in" - expect(page).to have_css "div.app-holder[data-react-class=Mastodon]" + expect(page).to have_css "div.app-holder" end end diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.jsx @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { render } from 'enzyme'; -import Avatar from '../../../app/assets/javascripts/components/components/avatar' +import Avatar from '../../../app/javascript/mastodon/components/avatar' describe('<Avatar />', () => { const src = '/path/to/image.jpg'; diff --git a/spec/javascript/components/button.test.jsx b/spec/javascript/components/button.test.jsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import Button from '../../../app/assets/javascripts/components/components/button'; +import Button from '../../../app/javascript/mastodon/components/button'; describe('<Button />', () => { it('renders a button element', () => { diff --git a/spec/javascript/components/display_name.test.jsx b/spec/javascript/components/display_name.test.jsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { render } from 'enzyme'; import Immutable from 'immutable'; -import DisplayName from '../../../app/assets/javascripts/components/components/display_name' +import DisplayName from '../../../app/javascript/mastodon/components/display_name' describe('<DisplayName />', () => { it('renders display name + account name', () => { diff --git a/spec/javascript/components/dropdown_menu.test.jsx b/spec/javascript/components/dropdown_menu.test.jsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; -import DropdownMenu from '../../../app/assets/javascripts/components/components/dropdown_menu'; +import DropdownMenu from '../../../app/javascript/mastodon/components/dropdown_menu'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; describe('<DropdownMenu />', () => { diff --git a/spec/javascript/components/features/ui/components/column.test.jsx b/spec/javascript/components/features/ui/components/column.test.jsx @@ -2,8 +2,8 @@ import { expect } from 'chai'; import { mount } from 'enzyme'; import sinon from 'sinon'; -import Column from '../../../../../../app/assets/javascripts/components/features/ui/components/column'; -import ColumnHeader from '../../../../../../app/assets/javascripts/components/features/ui/components/column_header'; +import Column from '../../../../../../app/javascript/mastodon/features/ui/components/column'; +import ColumnHeader from '../../../../../../app/javascript/mastodon/features/ui/components/column_header'; describe('<Column />', () => { describe('<ColumnHeader /> click handler', () => { diff --git a/spec/javascript/components/loading_indicator.test.jsx b/spec/javascript/components/loading_indicator.test.jsx @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { shallow } from 'enzyme'; -import LoadingIndicator from '../../../app/assets/javascripts/components/components/loading_indicator' +import LoadingIndicator from '../../../app/javascript/mastodon/components/loading_indicator' describe('<LoadingIndicator />', () => { diff --git a/yarn.lock b/yarn.lock @@ -99,20 +99,6 @@ webpack-dev-middleware "^1.6.0" webpack-hot-middleware "^2.13.2" -JSONStream@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.10.0.tgz#74349d0d89522b71f30f0a03ff9bd20ca6f12ac0" - dependencies: - jsonparse "0.0.5" - through ">=2.2.7 <3" - -JSONStream@^1.0.3: - version "1.2.1" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.2.1.tgz#32aa5790e799481083b49b4b7fa94e23bae69bf9" - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abab@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -146,14 +132,6 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn@^1.0.3: - version "1.2.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" - -acorn@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" - acorn@^3.0.0, acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -162,7 +140,7 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.11" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" -acorn@^5.0.1: +acorn@^5.0.0, acorn@^5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" @@ -183,7 +161,7 @@ ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" -ajv@^4.7.0: +ajv@^4.11.2, ajv@^4.7.0: version "4.11.3" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.3.tgz#ce30bdb90d1254f762c75af915fb3a63e7183d22" dependencies: @@ -222,6 +200,10 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +any-promise@^0.1.0, any-promise@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" + anymatch@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" @@ -270,10 +252,6 @@ array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" -array-filter@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" - array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -289,14 +267,6 @@ array-includes@^3.0.2: define-properties "^1.1.2" es-abstract "^1.5.0" -array-map@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" - -array-reduce@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -346,7 +316,7 @@ assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" -assert@^1.1.1, assert@^1.4.0: +assert@^1.1.1: version "1.4.1" resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" dependencies: @@ -364,12 +334,6 @@ ast-types@0.9.5: version "0.9.5" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.5.tgz#1a660a09945dbceb1f9c9cbb715002617424e04a" -astw@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astw/-/astw-2.0.0.tgz#08121ac8288d35611c0ceec663f6cd545604897d" - dependencies: - acorn "^1.0.3" - async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -378,6 +342,10 @@ async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async@0.2.x, async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + async@^0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -392,10 +360,6 @@ async@^2.1.2, async@^2.1.5: dependencies: lodash "^4.14.0" -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -404,15 +368,15 @@ autobind-decorator@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.3.3.tgz#41b1915ee742859c872b5d1743d10745254b83b4" -autoprefixer@^6.3.1, autoprefixer@^6.3.7: - version "6.5.0" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.5.0.tgz#910de0aa0f22af4c7d50367cbc9d4d412945162f" +autoprefixer@^6.3.1, autoprefixer@^6.3.7, autoprefixer@^6.7.7: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" dependencies: - browserslist "~1.4.0" - caniuse-db "^1.0.30000540" + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^5.2.2" + postcss "^5.2.16" postcss-value-parser "^3.2.3" aws-sign2@~0.6.0: @@ -450,15 +414,7 @@ babel-cli@^6.23.0: optionalDependencies: chokidar "^1.6.1" -babel-code-frame@^6.11.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^2.0.0" - -babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -466,43 +422,19 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" -babel-core@^6.0.14, babel-core@^6.23.0: - version "6.23.1" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df" +babel-core@^6.11.4, babel-core@^6.23.0, babel-core@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" dependencies: babel-code-frame "^6.22.0" - babel-generator "^6.23.0" - babel-helpers "^6.23.0" + babel-generator "^6.24.1" + babel-helpers "^6.24.1" babel-messages "^6.23.0" - babel-register "^6.23.0" + babel-register "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.23.0" - babel-traverse "^6.23.1" - babel-types "^6.23.0" - babylon "^6.11.0" - convert-source-map "^1.1.0" - debug "^2.1.1" - json5 "^0.5.0" - lodash "^4.2.0" - minimatch "^3.0.2" - path-is-absolute "^1.0.0" - private "^0.1.6" - slash "^1.0.0" - source-map "^0.5.0" - -babel-core@^6.11.4: - version "6.22.1" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.22.1.tgz#9c5fd658ba1772d28d721f6d25d968fc7ae21648" - dependencies: - babel-code-frame "^6.22.0" - babel-generator "^6.22.0" - babel-helpers "^6.22.0" - babel-messages "^6.22.0" - babel-register "^6.22.0" - babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.1" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" babylon "^6.11.0" convert-source-map "^1.1.0" debug "^2.1.1" @@ -523,25 +455,13 @@ babel-eslint@^7.2.2: babel-types "^6.23.0" babylon "^6.16.1" -babel-generator@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" - dependencies: - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - -babel-generator@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" +babel-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" dependencies: babel-messages "^6.23.0" babel-runtime "^6.22.0" - babel-types "^6.23.0" + babel-types "^6.24.1" detect-indent "^4.0.0" jsesc "^1.3.0" lodash "^4.2.0" @@ -573,22 +493,22 @@ babel-helper-builder-react-jsx@^6.8.0: esutils "^2.0.0" lodash "^4.2.0" -babel-helper-call-delegate@^6.22.0, babel-helper-call-delegate@^6.8.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz#119921b56120f17e9dae3f74b4f5cc7bcc1b37ef" +babel-helper-call-delegate@^6.24.1, babel-helper-call-delegate@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" dependencies: - babel-helper-hoist-variables "^6.22.0" + babel-helper-hoist-variables "^6.24.1" babel-runtime "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" -babel-helper-define-map@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.22.0.tgz#9544e9502b2d6dfe7d00ff60e82bd5a7a89e95b7" +babel-helper-define-map@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080" dependencies: - babel-helper-function-name "^6.22.0" + babel-helper-function-name "^6.24.1" babel-runtime "^6.22.0" - babel-types "^6.22.0" + babel-types "^6.24.1" lodash "^4.2.0" babel-helper-explode-assignable-expression@^6.22.0: @@ -608,36 +528,40 @@ babel-helper-explode-class@^6.22.0: babel-traverse "^6.22.0" babel-types "^6.22.0" -babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.8.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.22.0.tgz#51f1bdc4bb89b15f57a9b249f33d742816dcbefc" +babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.24.1, babel-helper-function-name@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" dependencies: - babel-helper-get-function-arity "^6.22.0" + babel-helper-get-function-arity "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" -babel-helper-get-function-arity@^6.22.0, babel-helper-get-function-arity@^6.8.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz#0beb464ad69dc7347410ac6ade9f03a50634f5ce" +babel-helper-get-function-arity@^6.24.1, babel-helper-get-function-arity@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" dependencies: babel-runtime "^6.22.0" - babel-types "^6.22.0" + babel-types "^6.24.1" -babel-helper-hoist-variables@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz#3eacbf731d80705845dd2e9718f600cfb9b4ba72" +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" dependencies: babel-runtime "^6.22.0" - babel-types "^6.22.0" + babel-types "^6.24.1" -babel-helper-optimise-call-expression@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.22.0.tgz#f8d5d4b40a6e2605a6a7f9d537b581bea3756d15" +babel-helper-is-react-class@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-helper-is-react-class/-/babel-helper-is-react-class-1.0.0.tgz#ef6f3678b05c76dbdeedadead7af98c2724d8431" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" dependencies: babel-runtime "^6.22.0" - babel-types "^6.22.0" + babel-types "^6.24.1" babel-helper-regex@^6.22.0: version "6.22.0" @@ -657,30 +581,31 @@ babel-helper-remap-async-to-generator@^6.22.0: babel-traverse "^6.22.0" babel-types "^6.22.0" -babel-helper-replace-supers@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.22.0.tgz#1fcee2270657548908c34db16bcc345f9850cf42" +babel-helper-replace-supers@^6.22.0, babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" dependencies: - babel-helper-optimise-call-expression "^6.22.0" - babel-messages "^6.22.0" + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" -babel-helpers@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.22.0.tgz#d275f55f2252b8101bff07bc0c556deda657392c" +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" dependencies: babel-runtime "^6.22.0" - babel-template "^6.22.0" + babel-template "^6.24.1" -babel-helpers@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.23.0.tgz#4f8f2e092d0b6a8808a4bde79c27f1e2ecf0d992" +babel-loader@7.x: + version "7.0.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.0.0.tgz#2e43a66bee1fff4470533d0402c8a4532fafbaf7" dependencies: - babel-runtime "^6.22.0" - babel-template "^6.23.0" + find-cache-dir "^0.1.1" + loader-utils "^1.0.2" + mkdirp "^0.5.1" babel-loader@^6.2.4: version "6.2.5" @@ -690,12 +615,6 @@ babel-loader@^6.2.4: mkdirp "^0.5.1" object-assign "^4.0.1" -babel-messages@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.22.0.tgz#36066a214f1217e4ed4164867669ecb39e3ea575" - dependencies: - babel-runtime "^6.22.0" - babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -716,6 +635,14 @@ babel-plugin-react-docgen@^1.4.2: lodash "4.x.x" react-docgen "^2.12.1" +babel-plugin-react-intl@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-react-intl/-/babel-plugin-react-intl-2.3.1.tgz#3d43912e824da005e08e8e8239d5ba784374bb00" + dependencies: + babel-runtime "^6.2.0" + intl-messageformat-parser "^1.2.0" + mkdirp "^0.5.1" + babel-plugin-react-transform@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-react-transform/-/babel-plugin-react-transform-2.0.2.tgz#515bbfa996893981142d90b1f9b1635de2995109" @@ -825,29 +752,29 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0, babel-plugin-trans dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-es2015-block-scoping@^6.22.0, babel-plugin-transform-es2015-block-scoping@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.22.0.tgz#00d6e3a0bebdcfe7536b9d653b44a9141e63e47e" +babel-plugin-transform-es2015-block-scoping@^6.22.0, babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.6.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" dependencies: babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" lodash "^4.2.0" -babel-plugin-transform-es2015-classes@^6.22.0, babel-plugin-transform-es2015-classes@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.22.0.tgz#54d44998fd823d9dca15292324161c331c1b6f14" +babel-plugin-transform-es2015-classes@^6.22.0, babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.6.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" dependencies: - babel-helper-define-map "^6.22.0" - babel-helper-function-name "^6.22.0" - babel-helper-optimise-call-expression "^6.22.0" - babel-helper-replace-supers "^6.22.0" - babel-messages "^6.22.0" + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" babel-plugin-transform-es2015-computed-properties@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.3.13: version "6.22.0" @@ -862,9 +789,9 @@ babel-plugin-transform-es2015-destructuring@6.16.0: dependencies: babel-runtime "^6.9.0" -babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.22.0.tgz#8e0af2f885a0b2cf999d47c4c1dd23ce88cfa4c6" +babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.23.0, babel-plugin-transform-es2015-destructuring@^6.6.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" dependencies: babel-runtime "^6.22.0" @@ -875,9 +802,9 @@ babel-plugin-transform-es2015-duplicate-keys@^6.22.0, babel-plugin-transform-es2 babel-runtime "^6.22.0" babel-types "^6.22.0" -babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.22.0.tgz#180467ad63aeea592a1caeee4bf1c8b3e2616265" +babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.23.0, babel-plugin-transform-es2015-for-of@^6.6.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" dependencies: babel-runtime "^6.22.0" @@ -895,7 +822,7 @@ babel-plugin-transform-es2015-literals@^6.22.0, babel-plugin-transform-es2015-li dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.8.0: +babel-plugin-transform-es2015-modules-amd@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21" dependencies: @@ -903,30 +830,38 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015 babel-runtime "^6.22.0" babel-template "^6.22.0" -babel-plugin-transform-es2015-modules-commonjs@^6.22.0, babel-plugin-transform-es2015-modules-commonjs@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.22.0.tgz#6ca04e22b8e214fb50169730657e7a07dc941145" +babel-plugin-transform-es2015-modules-amd@^6.24.1, babel-plugin-transform-es2015-modules-amd@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" dependencies: - babel-plugin-transform-strict-mode "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" -babel-plugin-transform-es2015-modules-systemjs@^6.12.0, babel-plugin-transform-es2015-modules-systemjs@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.22.0.tgz#810cd0cd025a08383b84236b92c6e31f88e644ad" +babel-plugin-transform-es2015-modules-commonjs@^6.22.0, babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.6.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" dependencies: - babel-helper-hoist-variables "^6.22.0" + babel-plugin-transform-strict-mode "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" -babel-plugin-transform-es2015-modules-umd@^6.12.0, babel-plugin-transform-es2015-modules-umd@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.22.0.tgz#60d0ba3bd23258719c64391d9bf492d648dc0fae" +babel-plugin-transform-es2015-modules-systemjs@^6.12.0, babel-plugin-transform-es2015-modules-systemjs@^6.22.0, babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" dependencies: - babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-helper-hoist-variables "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.12.0, babel-plugin-transform-es2015-modules-umd@^6.22.0, babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" babel-plugin-transform-es2015-object-super@^6.22.0, babel-plugin-transform-es2015-object-super@^6.3.13: version "6.22.0" @@ -946,16 +881,16 @@ babel-plugin-transform-es2015-parameters@6.17.0: babel-traverse "^6.16.0" babel-types "^6.16.0" -babel-plugin-transform-es2015-parameters@^6.22.0, babel-plugin-transform-es2015-parameters@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.22.0.tgz#57076069232019094f27da8c68bb7162fe208dbb" +babel-plugin-transform-es2015-parameters@^6.22.0, babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.6.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" dependencies: - babel-helper-call-delegate "^6.22.0" - babel-helper-get-function-arity "^6.22.0" + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.3.13: version "6.22.0" @@ -984,9 +919,9 @@ babel-plugin-transform-es2015-template-literals@^6.22.0, babel-plugin-transform- dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.6.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.22.0.tgz#87faf2336d3b6a97f68c4d906b0cd0edeae676e1" +babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.23.0, babel-plugin-transform-es2015-typeof-symbol@^6.6.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" dependencies: babel-runtime "^6.22.0" @@ -1033,26 +968,52 @@ babel-plugin-transform-react-constant-elements@6.9.1: dependencies: babel-runtime "^6.9.1" +babel-plugin-transform-react-constant-elements@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-constant-elements/-/babel-plugin-transform-react-constant-elements-6.23.0.tgz#2f119bf4d2cdd45eb9baaae574053c604f6147dd" + dependencies: + babel-runtime "^6.22.0" + babel-plugin-transform-react-display-name@^6.3.13: version "6.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.8.0.tgz#f7a084977383d728bdbdc2835bba0159577f660e" dependencies: babel-runtime "^6.0.0" -babel-plugin-transform-react-jsx-self@6.11.0, babel-plugin-transform-react-jsx-self@^6.11.0: +babel-plugin-transform-react-inline-elements@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-inline-elements/-/babel-plugin-transform-react-inline-elements-6.22.0.tgz#6687211a32b49a52f22c573a2b5504a25ef17c53" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-self@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.11.0.tgz#605c9450c1429f97a930f7e1dfe3f0d9d0dbd0f4" dependencies: babel-plugin-syntax-jsx "^6.8.0" babel-runtime "^6.9.0" -babel-plugin-transform-react-jsx-source@6.9.0, babel-plugin-transform-react-jsx-source@^6.3.13: +babel-plugin-transform-react-jsx-self@^6.11.0, babel-plugin-transform-react-jsx-self@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@6.9.0: version "6.9.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.9.0.tgz#af684a05c2067a86e0957d4f343295ccf5dccf00" dependencies: babel-plugin-syntax-jsx "^6.8.0" babel-runtime "^6.9.0" +babel-plugin-transform-react-jsx-source@^6.22.0, babel-plugin-transform-react-jsx-source@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + babel-plugin-transform-react-jsx@^6.3.13: version "6.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.8.0.tgz#94759942f70af18c617189aa7f3593f1644a71ab" @@ -1061,6 +1022,16 @@ babel-plugin-transform-react-jsx@^6.3.13: babel-plugin-syntax-jsx "^6.8.0" babel-runtime "^6.0.0" +babel-plugin-transform-react-pure-class-to-function@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-pure-class-to-function/-/babel-plugin-transform-react-pure-class-to-function-1.0.1.tgz#32a649c97d653250b419cfd1489331b0290d9ee4" + dependencies: + babel-helper-is-react-class "^1.0.0" + +babel-plugin-transform-react-remove-prop-types@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.3.tgz#fdff5c12933efc3ac979817adcdc0f612c5e3563" + babel-plugin-transform-regenerator@6.16.1: version "6.16.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.16.1.tgz#a75de6b048a14154aae14b0122756c5bed392f59" @@ -1081,12 +1052,12 @@ babel-plugin-transform-runtime@6.15.0: dependencies: babel-runtime "^6.9.0" -babel-plugin-transform-strict-mode@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz#e008df01340fdc87e959da65991b7e05970c8c7c" +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" dependencies: babel-runtime "^6.22.0" - babel-types "^6.22.0" + babel-types "^6.24.1" babel-polyfill@^6.23.0: version "6.23.0" @@ -1129,6 +1100,40 @@ babel-preset-env@0.0.6: babel-plugin-transform-regenerator "^6.6.0" browserslist "^1.4.0" +babel-preset-env@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^1.4.0" + invariant "^2.2.2" + babel-preset-es2015@^6.16.0, babel-preset-es2015@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835" @@ -1228,7 +1233,7 @@ babel-preset-stage-3@^6.22.0: babel-plugin-transform-exponentiation-operator "^6.22.0" babel-plugin-transform-object-rest-spread "^6.22.0" -babel-register@^6.22.0, babel-register@^6.23.0: +babel-register@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.23.0.tgz#c9aa3d4cca94b51da34826c4a0f9e08145d74ff3" dependencies: @@ -1240,6 +1245,18 @@ babel-register@^6.22.0, babel-register@^6.23.0: mkdirp "^0.5.1" source-map-support "^0.4.2" +babel-register@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" + dependencies: + babel-core "^6.24.1" + babel-runtime "^6.22.0" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + babel-runtime@6.11.6: version "6.11.6" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.11.6.tgz#6db707fef2d49c49bfa3cb64efdb436b518b8222" @@ -1247,53 +1264,53 @@ babel-runtime@6.11.6: core-js "^2.4.0" regenerator-runtime "^0.9.5" -babel-runtime@6.x.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.5.0, babel-runtime@^6.9.0, babel-runtime@^6.9.1, babel-runtime@^6.9.2: +babel-runtime@6.x.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.0, babel-runtime@^6.9.1, babel-runtime@^6.9.2: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-runtime@^6.2.0, babel-runtime@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611" dependencies: core-js "^2.4.0" regenerator-runtime "^0.10.0" -babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0, babel-template@^6.3.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638" +babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.24.1, babel-template@^6.3.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" dependencies: babel-runtime "^6.22.0" - babel-traverse "^6.23.0" - babel-types "^6.23.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.16.0, babel-traverse@^6.22.0, babel-traverse@^6.22.1, babel-traverse@^6.23.0, babel-traverse@^6.23.1: - version "6.23.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" +babel-traverse@^6.16.0, babel-traverse@^6.22.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" dependencies: babel-code-frame "^6.22.0" babel-messages "^6.23.0" babel-runtime "^6.22.0" - babel-types "^6.23.0" + babel-types "^6.24.1" babylon "^6.15.0" debug "^2.2.0" globals "^9.0.0" invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.16.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0, babel-types@^6.9.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" +babel-types@^6.16.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.9.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" dependencies: babel-runtime "^6.22.0" esutils "^2.0.2" lodash "^4.2.0" to-fast-properties "^1.0.1" -babelify@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5" - dependencies: - babel-core "^6.0.14" - object-assign "^4.0.0" - babylon@^6.11.0, babylon@^6.15.0, babylon@^6.16.1: version "6.16.1" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" @@ -1308,18 +1325,26 @@ backoff@^2.4.1: dependencies: precond "0.2" +balanced-match@0.1.0, balanced-match@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + +balanced-match@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" + balanced-match@^0.4.1, balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" -balanced-match@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" - base64-js@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" +batch@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" + bcrypt-pbkdf@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" @@ -1338,7 +1363,7 @@ bindings@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" -bl@^1.0.0, bl@~1.1.2: +bl@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" dependencies: @@ -1383,22 +1408,6 @@ brorand@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5" -browser-pack@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.1.tgz#779887c792eaa1f64a46a22c8f1051cdcd96755f" - dependencies: - JSONStream "^1.0.3" - combine-source-map "~0.7.1" - defined "^1.0.0" - through2 "^2.0.0" - umd "^3.0.0" - -browser-resolve@^1.11.0, browser-resolve@^1.7.0: - version "1.11.2" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" - dependencies: - resolve "1.1.7" - browser-stdout@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" @@ -1419,14 +1428,6 @@ browserify-aes@^1.0.0, browserify-aes@^1.0.4: evp_bytestokey "^1.0.0" inherits "^2.0.1" -browserify-cache-api@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/browserify-cache-api/-/browserify-cache-api-3.0.1.tgz#96247e853f068fd6e0d45cc73f0bb2cd9778ef02" - dependencies: - async "^1.5.2" - through2 "^2.0.0" - xtend "^4.0.0" - browserify-cipher@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" @@ -1443,15 +1444,6 @@ browserify-des@^1.0.0: des.js "^1.0.0" inherits "^2.0.1" -browserify-incremental@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/browserify-incremental/-/browserify-incremental-3.1.1.tgz#0713cb7587247a632a9f08cf1bd169b878b62a8a" - dependencies: - JSONStream "^0.10.0" - browserify-cache-api "^3.0.0" - through2 "^2.0.0" - xtend "^4.0.0" - browserify-rsa@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" @@ -1471,69 +1463,18 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" -browserify-zlib@^0.1.4, browserify-zlib@~0.1.2: +browserify-zlib@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" dependencies: pako "~0.2.0" -browserify@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.1.0.tgz#0508cc1e7bf4c152312c2fa523e676c0b0b92311" - dependencies: - JSONStream "^1.0.3" - assert "^1.4.0" - browser-pack "^6.0.1" - browser-resolve "^1.11.0" - browserify-zlib "~0.1.2" - buffer "^5.0.2" - cached-path-relative "^1.0.0" - concat-stream "~1.5.1" - console-browserify "^1.1.0" - constants-browserify "~1.0.0" - crypto-browserify "^3.0.0" - defined "^1.0.0" - deps-sort "^2.0.0" - domain-browser "~1.1.0" - duplexer2 "~0.1.2" - events "~1.1.0" - glob "^7.1.0" - has "^1.0.0" - htmlescape "^1.1.0" - https-browserify "~0.0.0" - inherits "~2.0.1" - insert-module-globals "^7.0.0" - labeled-stream-splicer "^2.0.0" - module-deps "^4.0.8" - os-browserify "~0.1.1" - parents "^1.0.1" - path-browserify "~0.0.0" - process "~0.11.0" - punycode "^1.3.2" - querystring-es3 "~0.2.0" - read-only-stream "^2.0.0" - readable-stream "^2.0.2" - resolve "^1.1.4" - shasum "^1.0.0" - shell-quote "^1.6.1" - stream-browserify "^2.0.0" - stream-http "^2.0.0" - string_decoder "~0.10.0" - subarg "^1.0.0" - syntax-error "^1.1.1" - through2 "^2.0.0" - timers-browserify "^1.0.1" - tty-browserify "~0.0.0" - url "~0.11.0" - util "~0.10.1" - vm-browserify "~0.0.1" - xtend "^4.0.0" - -browserslist@^1.4.0, browserslist@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.4.0.tgz#9cfdcf5384d9158f5b70da2aa00b30e8ff019049" +browserslist@^1.4.0, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" dependencies: - caniuse-db "^1.0.30000539" + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" buffer-shims@^1.0.0: version "1.0.0" @@ -1555,13 +1496,6 @@ buffer@^4.3.0, buffer@^4.9.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.5.tgz#35c9393244a90aff83581063d16f0882cecc9418" - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - bufferutil@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-2.0.1.tgz#8de37f5a300730c305fc3edd9f93348ee8a46288" @@ -1578,9 +1512,9 @@ builtin-status-codes@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz#6f22003baacf003ccd287afe6872151fddc58579" -cached-path-relative@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +bytes@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" caller-path@^0.1.0: version "0.1.0" @@ -1611,9 +1545,9 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" -caniuse-db@^1.0.30000539, caniuse-db@^1.0.30000540: - version "1.0.30000554" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000554.tgz#cd1dbe423d00b6203ba93f05973a476428dec919" +caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000664" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000664.tgz#e16316e5fdabb9c7209b2bf0744ffc8a14201f22" case-sensitive-paths-webpack-plugin@^1.1.2: version "1.1.4" @@ -1676,7 +1610,7 @@ cheerio@^0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@^1.0.0, chokidar@^1.4.3, chokidar@^1.6.1: +chokidar@^1.0.0, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" dependencies: @@ -1771,6 +1705,16 @@ code-point-at@^1.0.0: dependencies: number-is-nan "^1.0.0" +coffee-loader@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/coffee-loader/-/coffee-loader-0.7.3.tgz#fadbc6efd6fc7ecc88c5b3046a2c292066bcb54a" + dependencies: + loader-utils "^1.0.2" + +coffee-script@^1.12.5: + version "1.12.5" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.5.tgz#809f4585419112bbfe46a073ad7543af18c27346" + collapse-white-space@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.2.tgz#9c463fb9c6d190d2dcae21a356a01bcae9eeef6d" @@ -1809,15 +1753,6 @@ colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" -combine-source-map@~0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" - dependencies: - convert-source-map "~1.1.0" - inline-source-map "~0.6.0" - lodash.memoize "~3.0.3" - source-map "~0.5.3" - combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" @@ -1840,11 +1775,41 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" +complex.js@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.1.tgz#ea90c7a05aeceaf3a376d2c0f6a78421727d6879" + +compressible@~2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" + dependencies: + mime-db ">= 1.27.0 < 2" + +compression-webpack-plugin@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-0.4.0.tgz#811de04215f811ea6a12d4d8aed8457d758f13ac" + dependencies: + async "0.2.x" + webpack-sources "^0.1.0" + optionalDependencies: + node-zopfli "^2.0.0" + +compression@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" + dependencies: + accepts "~1.3.3" + bytes "2.3.0" + compressible "~2.0.8" + debug "~2.2.0" + on-headers "~1.0.1" + vary "~1.1.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1: +concat-stream@^1.4.7, concat-stream@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -1866,6 +1831,10 @@ configstore@^2.0.0: write-file-atomic "^1.1.2" xdg-basedir "^2.0.0" +connect-history-api-fallback@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169" + console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -1876,7 +1845,7 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" -constants-browserify@^1.0.0, constants-browserify@~1.0.0: +constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -1896,10 +1865,6 @@ convert-source-map@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" -convert-source-map@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1983,7 +1948,7 @@ crypto-browserify@3.3.0: ripemd160 "0.2.0" sha.js "2.2.6" -crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: +crypto-browserify@^3.11.0: version "3.11.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" dependencies: @@ -1998,11 +1963,20 @@ crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" +css-color-function@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "~0.7.4" + rgb "~0.1.0" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" -css-loader@^0.26.1, css-loader@^0.26.2: +css-loader@^0.26.1: version "0.26.2" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.26.2.tgz#a9cd4c2b1a559b45d8efc04fc311ab5d2aaccb9d" dependencies: @@ -2019,6 +1993,23 @@ css-loader@^0.26.1, css-loader@^0.26.2: postcss-modules-values "^1.1.0" source-list-map "^0.1.7" +css-loader@^0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.0.tgz#417cfa9789f8cde59a30ccbf3e4da7a806889bad" + dependencies: + babel-code-frame "^6.11.0" + css-selector-tokenizer "^0.7.0" + cssnano ">=2.6.1 <4" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.0.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.0.0" + postcss-modules-local-by-default "^1.0.1" + postcss-modules-scope "^1.0.0" + postcss-modules-values "^1.1.0" + source-list-map "^0.1.7" + css-select@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" @@ -2138,10 +2129,18 @@ debug@2.2.0, debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: dependencies: ms "0.7.1" +debug@~0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decimal.js@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.1.1.tgz#1adcad7d70d7a91c426d756f1eb6566c3be6cbcf" + deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" @@ -2160,6 +2159,12 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +defaults@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + define-properties@^1.1.1, define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" @@ -2195,15 +2200,6 @@ depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" -deps-sort@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" - dependencies: - JSONStream "^1.0.3" - shasum "^1.0.0" - subarg "^1.0.0" - through2 "^2.0.0" - des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -2221,13 +2217,6 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detective@^4.0.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.3.1.tgz#9fb06dd1ee8f0ea4dbcc607cda39d9ce1d4f726f" - dependencies: - acorn "^1.0.3" - defined "^1.0.0" - diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" @@ -2265,7 +2254,7 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "~1.1.1" entities "~1.1.1" -domain-browser@^1.1.1, domain-browser@~1.1.0: +domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" @@ -2304,12 +2293,6 @@ double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" -duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - dependencies: - readable-stream "^2.0.2" - ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -2324,6 +2307,10 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +electron-to-chromium@^1.2.7: + version "1.3.8" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.8.tgz#b2c8a2c79bb89fbbfd3724d9555e15095b5f5fb6" + element-class@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" @@ -2604,7 +2591,7 @@ esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@~3.1.0: +esprima@^3.1.1, esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -2648,10 +2635,20 @@ event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" -events@^1.0.0, events@^1.1.1, events@~1.1.0: +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.0, events@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" +eventsource@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + dependencies: + original ">=0.0.5" + eventsource@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.2.1.tgz#662bf85f376e73b5c34c2ee17db566b8419a6232" @@ -2729,6 +2726,15 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extract-text-webpack-plugin@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.0.tgz#69315b885f876dbf96d3819f6a9f1cca7aebf159" + dependencies: + ajv "^4.11.2" + async "^2.1.2" + loader-utils "^1.0.2" + webpack-sources "^0.1.0" + extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" @@ -2741,6 +2747,18 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + dependencies: + websocket-driver ">=0.5.1" + fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.12" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" @@ -2767,6 +2785,12 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +file-loader@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.1.tgz#6b328ee1234a729e4e47d36375dd6d35c0e1db84" + dependencies: + loader-utils "^1.0.2" + file-loader@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.9.0.tgz#1d2daddd424ce6d1b07cfe3f79731bed3617ab42" @@ -2831,6 +2855,10 @@ follow-redirects@1.0.0: dependencies: debug "^2.2.0" +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + for-in@^0.1.3, for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -2849,14 +2877,6 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.0.0.tgz#6f0aebadcc5da16c13e1ecc11137d85f9b883b25" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.11" - form-data@~2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" @@ -2875,10 +2895,39 @@ forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" +fraction.js@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.0.tgz#73974e2f8b51ef709536d624cc90782e2bb61274" + fresh@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" +fs-extra@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-promise@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.3.1.tgz#bf34050368f24d6dc9dfc6688ab5cead8f86842a" + dependencies: + any-promise "~0.1.0" + fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" @@ -2996,7 +3045,7 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.0.5, glob@^7.0.0: +glob@7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" dependencies: @@ -3007,7 +3056,17 @@ glob@7.0.5, glob@^7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@~7.1.1: +glob@^5.0.3: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -3022,6 +3081,17 @@ globals@^9.0.0, globals@^9.14.0: version "9.14.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" +globby@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-3.0.1.tgz#2094af8421e19152150d5893eb6416b312d9a22f" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^5.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^1.0.0" + globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -3041,7 +3111,13 @@ globule@^1.0.0: lodash "~4.16.4" minimatch "~3.0.2" -graceful-fs@^4.1.2, graceful-fs@^4.1.4: +gonzales-pe@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.0.3.tgz#36148e18e267184fbfdc929af28f29ad9fbf9746" + dependencies: + minimist "1.1.x" + +graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29" @@ -3053,6 +3129,10 @@ growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +handle-thing@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + har-validator@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" @@ -3076,7 +3156,7 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" -has@^1.0.0, has@^1.0.1: +has@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" dependencies: @@ -3125,6 +3205,15 @@ 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" +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + 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" @@ -3145,10 +3234,6 @@ html@^1.0.0: dependencies: concat-stream "^1.4.7" -htmlescape@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" - htmlparser2@^3.9.1: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -3160,7 +3245,11 @@ htmlparser2@^3.9.1: inherits "^2.0.1" readable-stream "^2.0.2" -http-errors@~1.5.1: +http-deceiver@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-errors@~1.5.0, http-errors@~1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" dependencies: @@ -3172,6 +3261,22 @@ http-link-header@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.8.0.tgz#a22b41a0c9b1e2d8fac1bf1b697c6bd532d5f5e4" +http-proxy-middleware@~0.17.4: + version "0.17.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + dependencies: + http-proxy "^1.16.2" + is-glob "^3.1.0" + lodash "^4.17.2" + micromatch "^2.3.11" + +http-proxy@^1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -3180,7 +3285,7 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-browserify@0.0.1, https-browserify@~0.0.0: +https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" @@ -3245,12 +3350,6 @@ ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inline-source-map@~0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" - dependencies: - source-map "~0.5.3" - inquirer@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" @@ -3269,19 +3368,6 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" -insert-module-globals@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" - dependencies: - JSONStream "^1.0.3" - combine-source-map "~0.7.1" - concat-stream "~1.5.1" - is-buffer "^1.1.0" - lexical-scope "^1.2.0" - process "~0.11.0" - through2 "^2.0.0" - xtend "^4.0.0" - interpret@^0.6.4: version "0.6.6" resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" @@ -3298,7 +3384,7 @@ intl-format-cache@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.0.5.tgz#b484cefcb9353f374f25de389a3ceea1af18d7c9" -intl-messageformat-parser@1.2.0: +intl-messageformat-parser@1.2.0, intl-messageformat-parser@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.2.0.tgz#5906b7f953ab7470e0dc8549097b648b991892ff" @@ -3314,7 +3400,7 @@ intl-relativeformat@^1.3.0: dependencies: intl-messageformat "1.3.0" -invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1: +invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3342,7 +3428,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.0: +is-buffer@^1.0.2: version "1.1.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" @@ -3382,6 +3468,10 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -3404,6 +3494,12 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.15.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" @@ -3505,10 +3601,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" -isarray@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" @@ -3540,6 +3632,16 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" +jquery-ujs@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.2.tgz#6a8ef1020e6b6dda385b90a4bddc128c21c56397" + dependencies: + jquery ">=1.8.0" + +jquery@>=1.8.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + js-base64@^2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" @@ -3548,15 +3650,18 @@ js-tokens@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.3.tgz#14e56eb68c8f1a92c43d59f5014ec29dc20f2ae1" -js-tokens@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" - js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@~3.6.1: +js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + +js-yaml@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" dependencies: @@ -3613,17 +3718,11 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stable-stringify@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" - dependencies: - jsonify "~0.0.0" - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json3@3.3.2: +json3@3.3.2, json3@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" @@ -3631,18 +3730,20 @@ json5@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.0.tgz#9b20715b026cbe3778fd769edccd822d8332a5b2" +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" -jsonparse@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64" - -jsonparse@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" - jsonpointer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" @@ -3677,13 +3778,11 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" -labeled-stream-splicer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" - dependencies: - inherits "^2.0.1" - isarray "~0.0.1" - stream-splicer "^2.0.0" +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" lazy-cache@^0.2.3: version "0.2.7" @@ -3706,12 +3805,6 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lexical-scope@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" - dependencies: - astw "^2.0.0" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -3735,7 +3828,15 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0. json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.1, loader-utils@^1.0.2: +loader-utils@^1.0.1, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +loader-utils@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.0.2.tgz#a9f923c865a974623391a8602d031137fad74830" dependencies: @@ -3818,7 +3919,7 @@ lodash.create@3.1.1: lodash._basecreate "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.defaults@^4.0.1: +lodash.defaults@^4.0.1, lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -3858,10 +3959,6 @@ lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" -lodash.memoize@~3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" - lodash.merge@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" @@ -3898,7 +3995,7 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0: +lodash@4.x.x, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3914,18 +4011,18 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - -loose-envify@^1.1.0, loose-envify@^1.2.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.2.0.tgz#69a65aad3de542cf4ee0f4fe74e8e33c709ccb0f" dependencies: js-tokens "^1.0.1" +loose-envify@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -3962,6 +4059,17 @@ math-expression-evaluator@^1.2.14: dependencies: lodash.indexof "^4.0.5" +mathjs@^3.11.5: + version "3.12.1" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.12.1.tgz#a165abdbc6b55da1ec600b85311e2962850551b5" + dependencies: + complex.js "2.0.1" + decimal.js "7.1.1" + fraction.js "4.0.0" + seed-random "2.2.0" + tiny-emitter "1.0.2" + typed-function "0.10.5" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4007,7 +4115,7 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5: +micromatch@^2.1.5, micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -4032,11 +4140,15 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" +"mime-db@>= 1.27.0 < 2": + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + mime-db@~1.26.0: version "1.26.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" -mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: version "2.1.14" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" dependencies: @@ -4054,7 +4166,7 @@ minimalistic-assert@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: @@ -4064,7 +4176,11 @@ minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.1.x: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + +minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -4075,7 +4191,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -4101,26 +4217,6 @@ mocha@^3.2.0: mkdirp "0.5.1" supports-color "3.1.2" -module-deps@^4.0.8: - version "4.1.1" - resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" - dependencies: - JSONStream "^1.0.3" - browser-resolve "^1.7.0" - cached-path-relative "^1.0.0" - concat-stream "~1.5.0" - defined "^1.0.0" - detective "^4.0.0" - duplexer2 "^0.1.2" - inherits "^2.0.1" - parents "^1.0.0" - readable-stream "^2.0.2" - resolve "^1.1.3" - stream-combiner2 "^1.1.1" - subarg "^1.0.0" - through2 "^2.0.0" - xtend "^4.0.0" - ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -4133,7 +4229,7 @@ mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" -nan@^2.3.0, nan@^2.3.2, nan@~2.5.0: +nan@^2.0.0, nan@^2.3.0, nan@^2.3.2, nan@~2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" @@ -4236,7 +4332,7 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-pre-gyp@^0.6.29: +node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4: version "0.6.30" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.30.tgz#64d3073a6f573003717ccfe30c89023297babba1" dependencies: @@ -4273,9 +4369,14 @@ node-sass@^4.5.2: sass-graph "^2.1.1" stdout-stream "^1.4.0" -node-uuid@~1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" +node-zopfli@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-zopfli/-/node-zopfli-2.0.2.tgz#a7a473ae92aaea85d4c68d45bbf2c944c46116b8" + dependencies: + commander "^2.8.1" + defaults "^1.0.2" + nan "^2.0.0" + node-pre-gyp "^0.6.4" noop-logger@^0.1.1: version "0.1.1" @@ -4344,7 +4445,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@4.1.0, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@4.1.0, object-assign@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" @@ -4352,7 +4453,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4.1.1: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4404,12 +4505,20 @@ object.values@^1.0.3: function-bind "^1.0.2" has "^1.0.1" +obuf@^1.0.0, obuf@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" dependencies: ee-first "1.1.1" +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + once@^1.3.0, once@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4426,6 +4535,13 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" +opn@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + optimist@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -4444,7 +4560,7 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -original@^1.0.0: +original@>=0.0.5, original@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" dependencies: @@ -4454,10 +4570,6 @@ os-browserify@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" -os-browserify@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54" - os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -4492,14 +4604,8 @@ packet-reader@0.2.0: resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.2.0.tgz#819df4d010b82d5ea5671f8a1a3acf039bcd7700" pako@~0.2.0: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - -parents@^1.0.0, parents@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" - dependencies: - path-platform "~0.11.15" + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" parse-asn1@^5.0.0: version "5.0.0" @@ -4534,10 +4640,14 @@ parseurl@~1.3.0, parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" -path-browserify@0.0.0, path-browserify@~0.0.0: +path-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" +path-complete-extname@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-0.1.0.tgz#c454702669f31452f8193aa6168915fa31692f4a" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -4552,9 +4662,9 @@ path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" -path-platform@~0.11.15: - version "0.11.15" - resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" path-to-regexp@0.1.7: version "0.1.7" @@ -4625,12 +4735,22 @@ pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pinkie-promise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-1.0.0.tgz#d1da67f5482563bb7cf57f286ae2822ecfbf3670" + dependencies: + pinkie "^1.0.0" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" dependencies: pinkie "^2.0.0" +pinkie@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-1.0.0.tgz#5a47f28ba1015d0201bda7bf0f358e47bec8c7e4" + pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" @@ -4652,6 +4772,26 @@ podda@^1.2.1: babel-runtime "^6.11.6" immutable "^3.8.1" +portfinder@^1.0.9: + version "1.0.13" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +postcss-advanced-variables@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-advanced-variables/-/postcss-advanced-variables-1.2.2.tgz#90a6213262e66a050a368b4a9c5d4778d72dbd74" + dependencies: + postcss "^5.0.10" + +postcss-atroot@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/postcss-atroot/-/postcss-atroot-0.1.3.tgz#6752c0230c745140549345b2b0e30ebeda01a405" + dependencies: + postcss "^5.0.5" + postcss-calc@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" @@ -4660,6 +4800,15 @@ postcss-calc@^5.2.0: postcss-message-helpers "^2.0.0" reduce-css-calc "^1.2.6" +postcss-color-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-2.0.1.tgz#9ad226f550e8a7c7f8b8a77860545b6dd7f55241" + dependencies: + css-color-function "^1.2.0" + postcss "^5.0.4" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.0" + postcss-colormin@^2.1.8: version "2.2.1" resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.1.tgz#dc5421b6ae6f779ef6bfd47352b94abe59d0316b" @@ -4675,6 +4824,27 @@ postcss-convert-values@^2.3.4: postcss "^5.0.11" postcss-value-parser "^3.1.2" +postcss-custom-media@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-5.0.1.tgz#138d25a184bf2eb54de12d55a6c01c30a9d8bd81" + dependencies: + postcss "^5.0.0" + +postcss-custom-properties@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-5.0.2.tgz#9719d78f2da9cf9f53810aebc23d4656130aceb1" + dependencies: + balanced-match "^0.4.2" + postcss "^5.0.0" + +postcss-custom-selectors@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-3.0.0.tgz#8f81249f5ed07a8d0917cf6a39fe5b056b7f96ac" + dependencies: + balanced-match "^0.2.0" + postcss "^5.0.0" + postcss-selector-matches "^2.0.0" + postcss-discard-comments@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" @@ -4707,6 +4877,12 @@ postcss-discard-unused@^2.2.1: postcss "^5.0.14" uniqs "^2.0.0" +postcss-extend@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-extend/-/postcss-extend-1.0.5.tgz#5ea98bf787ba3cacf4df4609743f80a833b1d0e7" + dependencies: + postcss "^5.0.4" + postcss-filter-plugins@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" @@ -4714,7 +4890,7 @@ postcss-filter-plugins@^2.0.0: postcss "^5.0.4" uniqid "^4.0.0" -postcss-load-config@^1.0.0: +postcss-load-config@^1.0.0, postcss-load-config@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" dependencies: @@ -4746,6 +4922,21 @@ postcss-loader@1.1.0: postcss "^5.2.5" postcss-load-config "^1.0.0" +postcss-loader@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-1.3.3.tgz#a621ea1fa29062a83972a46f54486771301916eb" + dependencies: + loader-utils "^1.0.2" + object-assign "^4.1.1" + postcss "^5.2.15" + postcss-load-config "^1.2.0" + +postcss-media-minmax@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-2.1.2.tgz#444c5cf8926ab5e4fd8a2509e9297e751649cdf8" + dependencies: + postcss "^5.0.4" + postcss-merge-idents@^2.1.5: version "2.1.7" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" @@ -4803,6 +4994,14 @@ postcss-minify-selectors@^2.0.4: postcss "^5.0.14" postcss-selector-parser "^2.0.0" +postcss-mixins@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-2.1.1.tgz#b141a0803efa8e2d744867f8d91596890cf9241b" + dependencies: + globby "^3.0.1" + postcss "^5.0.10" + postcss-simple-vars "^1.0.1" + postcss-modules-extract-imports@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.1.tgz#8fb3fef9a6dd0420d3f6d4353cf1ff73f2b2a341" @@ -4830,6 +5029,18 @@ postcss-modules-values@^1.1.0: icss-replace-symbols "^1.0.2" postcss "^5.0.14" +postcss-nested@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-1.0.1.tgz#91f28f4e6e23d567241ac154558a0cfab4cc0d8f" + dependencies: + postcss "^5.2.17" + +postcss-nesting@^2.0.6: + version "2.3.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-2.3.1.tgz#94a6b6a4ef707fbec20a87fee5c957759b4e01cf" + dependencies: + postcss "^5.0.19" + postcss-normalize-charset@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.0.tgz#2fbd30e12248c442981d31ea2484d46fd0628970" @@ -4852,6 +5063,24 @@ postcss-ordered-values@^2.1.0: postcss "^5.0.4" postcss-value-parser "^3.0.1" +postcss-partial-import@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-partial-import/-/postcss-partial-import-1.3.0.tgz#2f4b773a76c7b0a69b389dcf475c4d362d0d2576" + dependencies: + fs-extra "^0.24.0" + fs-promise "^0.3.1" + object-assign "^4.0.1" + postcss "^5.0.5" + string-hash "^1.1.0" + +postcss-property-lookup@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/postcss-property-lookup/-/postcss-property-lookup-1.2.1.tgz#30450a1361b7aae758bbedd5201fbe057bb8270b" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + tcomb "^2.5.1" + postcss-reduce-idents@^2.2.2: version "2.3.0" resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.3.0.tgz#a697b52953ed6825ffea404e26a4f105d8b8d569" @@ -4872,6 +5101,34 @@ postcss-reduce-transforms@^1.0.3: postcss "^5.0.8" postcss-value-parser "^3.0.1" +postcss-sass@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.1.0.tgz#0d2a655b5d241ec8f419bb3da38de5ca11746ddb" + dependencies: + gonzales-pe "^4.0.3" + mathjs "^3.11.5" + postcss "^5.2.6" + +postcss-scss@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-0.4.1.tgz#ad771b81f0f72f5f4845d08aa60f93557653d54c" + dependencies: + postcss "^5.2.13" + +postcss-selector-matches@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-2.0.5.tgz#fa0f43be57b68e77aa4cd11807023492a131027f" + dependencies: + balanced-match "^0.4.2" + postcss "^5.0.0" + +postcss-selector-not@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-2.0.0.tgz#c73ad21a3f75234bee7fee269e154fd6a869798d" + dependencies: + balanced-match "^0.2.0" + postcss "^5.0.0" + postcss-selector-parser@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.1.tgz#fdbf696103b12b0a64060e5610507f410491f7c8" @@ -4880,6 +5137,28 @@ postcss-selector-parser@^2.0.0: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-simple-vars@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" + dependencies: + postcss "^5.0.13" + +postcss-smart-import@^0.6.12: + version "0.6.12" + resolved "https://registry.yarnpkg.com/postcss-smart-import/-/postcss-smart-import-0.6.12.tgz#6bd50846547383d15332206615265b87b4a6ae89" + dependencies: + babel-runtime "^6.23.0" + lodash "^4.17.4" + object-assign "^4.1.1" + postcss "^5.2.17" + postcss-sass "^0.1.0" + postcss-scss "^0.4.1" + postcss-value-parser "^3.3.0" + promise-each "^2.2.0" + read-cache "^1.0.0" + resolve "^1.3.3" + sugarss "^0.2.0" + postcss-svgo@^2.1.1: version "2.1.5" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.5.tgz#46fc0363f01bab6a36a9abb01c229fcc45363094" @@ -4897,7 +5176,7 @@ postcss-unique-selectors@^2.0.2: postcss "^5.0.4" uniqs "^2.0.0" -postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.1.3, postcss-value-parser@^3.2.3: +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.1.3, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" @@ -4908,24 +5187,15 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.5: - version "5.2.15" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.15.tgz#a9e8685e50e06cc5b3fdea5297273246c26f5b30" +postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.13, postcss@^5.2.15, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.4, postcss@^5.2.5, postcss@^5.2.6: + version "5.2.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" source-map "^0.5.6" supports-color "^3.2.3" -postcss@^5.0.6, postcss@^5.2.2: - version "5.2.4" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.4.tgz#8eb4bee3e5c4e091585b116df32d8db24a535f21" - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.1.2" - postgres-array@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.2.tgz#8e0b32eb03bf77a5c0a7851e0441c169a256a238" @@ -4966,6 +5236,27 @@ precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" +precss@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/precss/-/precss-1.4.0.tgz#8d7c3ae70f10a00a3955287f85a66e0f8b31cda3" + dependencies: + postcss "^5.0.10" + postcss-advanced-variables "1.2.2" + postcss-atroot "^0.1.2" + postcss-color-function "^2.0.0" + postcss-custom-media "^5.0.0" + postcss-custom-properties "^5.0.0" + postcss-custom-selectors "^3.0.0" + postcss-extend "^1.0.1" + postcss-media-minmax "^2.1.0" + postcss-mixins "^2.1.0" + postcss-nested "^1.0.0" + postcss-nesting "^2.0.6" + postcss-partial-import "^1.3.0" + postcss-property-lookup "^1.1.3" + postcss-selector-matches "^2.0.0" + postcss-selector-not "^2.0.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4986,7 +5277,7 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -process@^0.11.0, process@~0.11.0: +process@^0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" @@ -4994,6 +5285,12 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +promise-each@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/promise-each/-/promise-each-2.2.0.tgz#3353174eff2694481037e04e01f77aa0fb6d1b60" + dependencies: + any-promise "^0.1.0" + promise@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" @@ -5042,7 +5339,7 @@ punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" -punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: +punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -5050,11 +5347,11 @@ q@^1.1.2: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -qs@6.2.0, qs@^6.1.0, qs@^6.2.0, qs@~6.2.0: +qs@6.2.0, qs@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" -qs@~6.3.0: +qs@^6.2.0, qs@~6.3.0: version "6.3.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.1.tgz#918c0b3bcd36679772baf135b1acb4c1651ed79d" @@ -5071,7 +5368,7 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -querystring-es3@^0.2.0, querystring-es3@~0.2.0: +querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -5089,6 +5386,13 @@ raf@^3.1.0: dependencies: performance-now "~0.2.0" +rails-erb-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rails-erb-loader/-/rails-erb-loader-5.0.0.tgz#33fcfbae3fd4584801283c4d5bc1195ae7b56723" + dependencies: + loader-utils "^1.1.0" + lodash.defaults "^4.2.0" + randomatic@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.5.tgz#5e9ef5f2d573c67bd2b8124ae90b5156e457840b" @@ -5172,7 +5476,7 @@ react-docgen@^2.12.1: node-dir "^0.1.10" recast "^0.11.5" -react-dom@^15.5.4: +react-dom@^15.4.2, react-dom@^15.5.4: version "15.5.4" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" dependencies: @@ -5208,12 +5512,29 @@ react-immutable-proptypes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" +react-immutable-pure-component@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/react-immutable-pure-component/-/react-immutable-pure-component-0.0.4.tgz#a7c438167e5b82b5231e4ec9246827ebe3257629" + dependencies: + immutable "^3.8.1" + react "^15.4.2" + react-dom "^15.4.2" + react-inspector@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-1.1.0.tgz#8d55bb94ffc9fd3982a222eb257dbe9cdd4f1b87" dependencies: is-dom "^1.0.5" +react-intl-translations-manager@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-intl-translations-manager/-/react-intl-translations-manager-5.0.0.tgz#3c78d3e3e44c5804d7a15c60e89c3aefd9d06615" + dependencies: + chalk "^1.1.3" + glob "^7.0.3" + json-stable-stringify "^1.0.1" + mkdirp "^0.5.1" + react-intl@^2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.1.5.tgz#f9795ea34b790dcb5d0d8ef7060dddbe85bf8763" @@ -5364,7 +5685,7 @@ react-virtualized@^8.11.4: dom-helpers "^2.4.0 || ^3.0.0" loose-envify "^1.3.0" -react@^15.5.4: +react@^15.4.2, react@^15.5.4: version "15.5.4" resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047" dependencies: @@ -5373,11 +5694,11 @@ react@^15.5.4: object-assign "^4.1.0" prop-types "^15.5.7" -read-only-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" dependencies: - readable-stream "^2.0.2" + pify "^2.3.0" read-pkg-up@^1.0.1: version "1.0.1" @@ -5572,7 +5893,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2, request@2.x, request@^2.79.0: +request@2, request@2.x, request@^2.74.0, request@^2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" dependencies: @@ -5597,32 +5918,6 @@ request@2, request@2.x, request@^2.79.0: tunnel-agent "~0.4.1" uuid "^3.0.0" -request@^2.74.0: - version "2.75.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - bl "~1.1.2" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.0.0" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - node-uuid "~1.4.7" - oauth-sign "~0.8.1" - qs "~6.2.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5642,7 +5937,7 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" -requires-port@1.0.x: +requires-port@1.0.x, requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5654,9 +5949,11 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" -resolve@1.1.7, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +resolve@^1.1.6, resolve@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" + dependencies: + path-parse "^1.0.5" restore-cursor@^1.0.1: version "1.0.1" @@ -5665,6 +5962,10 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -5707,9 +6008,9 @@ sass-graph@^2.1.1: lodash "^4.0.0" yargs "^4.7.1" -sass-loader@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.2.tgz#96343a9f5c585780149321c7bda9e1da633d2c73" +sass-loader@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.3.tgz#33983b1f90d27ddab0e57d0dac403dce9bc7ecfd" dependencies: async "^2.1.5" clone-deep "^0.2.4" @@ -5732,6 +6033,14 @@ section-iterator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" +seed-random@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + "semver@2 || 3 || 4 || 5", semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -5767,6 +6076,18 @@ serve-favicon@^2.3.0: ms "0.7.1" parseurl "~1.3.0" +serve-index@^1.7.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b" + dependencies: + accepts "~1.3.3" + batch "0.5.3" + debug "~2.2.0" + escape-html "~1.0.3" + http-errors "~1.5.0" + mime-types "~2.1.11" + parseurl "~1.3.1" + serve-static@~1.11.2: version "1.11.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" @@ -5796,7 +6117,7 @@ sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" -sha.js@^2.3.6, sha.js@~2.4.4: +sha.js@^2.3.6: version "2.4.5" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.5.tgz#27d171efcc82a118b99639ff581660242b506e7c" dependencies: @@ -5821,22 +6142,6 @@ shallowequal@0.2.x, shallowequal@^0.2.2: dependencies: lodash.keys "^3.1.2" -shasum@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" - dependencies: - json-stable-stringify "~0.0.0" - sha.js "~2.4.4" - -shell-quote@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" - dependencies: - array-filter "~0.0.0" - array-map "~0.0.0" - array-reduce "~0.0.0" - jsonify "~0.0.0" - shelljs@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.4.tgz#b8f04b3a74ddfafea22acf98e0be45ded53d59c8" @@ -5892,6 +6197,24 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sockjs-client@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5" + dependencies: + debug "^2.2.0" + eventsource "0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.1" + +sockjs@0.3.18: + version "0.3.18" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" + dependencies: + faye-websocket "^0.10.0" + uuid "^2.0.2" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -5908,6 +6231,10 @@ source-list-map@^0.1.7, source-list-map@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.7.tgz#d4b5ce2a46535c72c7e8527c71a77d250618172e" +source-list-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4" + source-map-support@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.3.tgz#693c8383d4389a4569486987c219744dfc601685" @@ -5944,6 +6271,26 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdy-transport@^2.0.15: + version "2.0.18" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.18.tgz#43fc9c56be2cccc12bb3e2754aa971154e836ea6" + dependencies: + debug "^2.2.0" + hpack.js "^2.1.6" + obuf "^1.1.0" + readable-stream "^2.0.1" + wbuf "^1.4.0" + +spdy@^3.4.1: + version "3.4.4" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.4.tgz#e0406407ca90ff01b553eb013505442649f5a819" + dependencies: + debug "^2.2.0" + handle-thing "^1.2.4" + http-deceiver "^1.2.4" + select-hose "^2.0.0" + spdy-transport "^2.0.15" + split@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/split/-/split-1.0.0.tgz#c4395ce683abcd254bc28fe1dabb6e5c27dcffae" @@ -5983,21 +6330,14 @@ store@^1.3.20: version "1.3.20" resolved "https://registry.yarnpkg.com/store/-/store-1.3.20.tgz#13ea7e3fb2d6c239868265d686b1d84e99c5be3e" -stream-browserify@^2.0.0, stream-browserify@^2.0.1: +stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" dependencies: inherits "~2.0.1" readable-stream "^2.0.2" -stream-combiner2@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" - dependencies: - duplexer2 "~0.1.0" - readable-stream "^2.0.2" - -stream-http@^2.0.0, stream-http@^2.3.1: +stream-http@^2.3.1: version "2.4.0" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.4.0.tgz#9599aa8e263667ce4190e0dc04a1d065d3595a7e" dependencies: @@ -6007,17 +6347,14 @@ stream-http@^2.0.0, stream-http@^2.3.1: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-splicer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.2" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" +string-hash@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -6049,7 +6386,7 @@ string.prototype.padstart@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" -string_decoder@^0.10.25, string_decoder@~0.10.0, string_decoder@~0.10.x: +string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -6104,19 +6441,19 @@ style-loader@0.13.1: dependencies: loader-utils "^0.2.7" -style-loader@^0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.2.tgz#74533384cf698c7104c7951150b49717adc2f3bb" +style-loader@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.16.1.tgz#50e325258d4e78421dd9680636b41e8661595d10" dependencies: loader-utils "^1.0.2" -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" +sugarss@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-0.2.0.tgz#ac34237563327c6ff897b64742bf6aec190ad39e" dependencies: - minimist "^1.1.0" + postcss "^5.2.4" -supports-color@3.1.2, supports-color@^3.1.0, supports-color@^3.1.2: +supports-color@3.1.2, supports-color@^3.1.0, supports-color@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" dependencies: @@ -6156,12 +6493,6 @@ symbol-tree@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" -syntax-error@^1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.1.6.tgz#b4549706d386cc1c1dc7c2423f18579b6cade710" - dependencies: - acorn "^2.7.0" - table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -6220,33 +6551,28 @@ tar@^2.0.0, tar@~2.2.0, tar@~2.2.1: fstream "^1.0.2" inherits "2" +tcomb@^2.5.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" -through2@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" - dependencies: - readable-stream "~2.0.0" - xtend "~4.0.0" - -through@2, "through@>=2.2.7 <3", through@^2.3.6: +through@2, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" -timers-browserify@^1.0.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" - dependencies: - process "~0.11.0" - timers-browserify@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86" dependencies: setimmediate "^1.0.4" +tiny-emitter@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.0.2.tgz#8e49470d3f55f89e247210368a6bb9fb51aa1601" + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -6281,7 +6607,7 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" -tty-browserify@0.0.0, tty-browserify@~0.0.0: +tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -6314,6 +6640,10 @@ type-is@~1.6.14: media-typer "0.3.0" mime-types "~2.1.13" +typed-function@0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-0.10.5.tgz#2e0f18abd065219fab694a446a65c6d1981832c0" + typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -6322,7 +6652,16 @@ ua-parser-js@^0.7.9: version "0.7.10" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f" -uglify-js@^2.7.5, uglify-js@~2.7.3: +uglify-js@^2.8.5: + version "2.8.22" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.22.tgz#d54934778a8da14903fa29a326fb24c0ab51a1a0" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-js@~2.7.3: version "2.7.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" dependencies: @@ -6343,10 +6682,6 @@ ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" -umd@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e" - uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -6383,7 +6718,14 @@ url-parse@1.0.x: querystringify "0.0.x" requires-port "1.0.x" -url@^0.11.0, url@~0.11.0: +url-parse@^1.1.1: + version "1.1.8" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.8.tgz#7a65b3a8d57a1e86af6b4e2276e34774167c0156" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" dependencies: @@ -6412,7 +6754,7 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3, util@~0.10.1: +util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" dependencies: @@ -6422,7 +6764,7 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" -uuid@^2.0.1, uuid@^2.0.3: +uuid@^2.0.1, uuid@^2.0.2, uuid@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -6457,7 +6799,7 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" -vm-browserify@0.0.4, vm-browserify@~0.0.1: +vm-browserify@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" dependencies: @@ -6483,7 +6825,7 @@ watchpack@^0.2.1: chokidar "^1.0.0" graceful-fs "^4.1.2" -watchpack@^1.2.0: +watchpack@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" dependencies: @@ -6491,6 +6833,12 @@ watchpack@^1.2.0: chokidar "^1.4.3" graceful-fs "^4.1.2" +wbuf@^1.1.0, wbuf@^1.4.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" + dependencies: + minimalistic-assert "^1.0.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -6506,6 +6854,15 @@ webpack-core@~0.6.9: source-list-map "~0.1.7" source-map "~0.4.1" +webpack-dev-middleware@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" + dependencies: + memory-fs "~0.4.1" + mime "^1.3.4" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + webpack-dev-middleware@^1.6.0: version "1.8.4" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.8.4.tgz#e8765c9122887ce9e3abd4cc9c3eb31b61e0948d" @@ -6515,6 +6872,28 @@ webpack-dev-middleware@^1.6.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" +webpack-dev-server@^2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.5.tgz#31384ce81136be1080b4b4cde0eb9b90e54ee6cf" + dependencies: + ansi-html "0.0.7" + chokidar "^1.6.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + express "^4.13.3" + html-entities "^1.2.0" + http-proxy-middleware "~0.17.4" + opn "4.0.2" + portfinder "^1.0.9" + serve-index "^1.7.2" + sockjs "0.3.18" + sockjs-client "1.1.2" + spdy "^3.4.1" + strip-ansi "^3.0.0" + supports-color "^3.1.1" + webpack-dev-middleware "^1.10.2" + yargs "^6.0.0" + webpack-hot-middleware@^2.13.2: version "2.17.1" resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.17.1.tgz#0c8fbf6f93ff29c095d684b07ab6d6c0f2f951d7" @@ -6524,13 +6903,33 @@ webpack-hot-middleware@^2.13.2: querystring "^0.2.0" strip-ansi "^3.0.0" -webpack-sources@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.4.tgz#ccc2c817e08e5fa393239412690bb481821393cd" +webpack-manifest-plugin@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.1.0.tgz#6b6c718aade8a2537995784b46bd2e9836057caa" + dependencies: + fs-extra "^0.30.0" + lodash ">=3.5 <5" + +webpack-merge@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e" + dependencies: + lodash "^4.17.4" + +webpack-sources@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" dependencies: source-list-map "~0.1.7" source-map "~0.5.3" +webpack-sources@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb" + dependencies: + source-list-map "^1.1.1" + source-map "~0.5.3" + webpack@^1.13.1: version "1.14.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-1.14.0.tgz#54f1ffb92051a328a5b2057d6ae33c289462c823" @@ -6551,11 +6950,11 @@ webpack@^1.13.1: watchpack "^0.2.1" webpack-core "~0.6.9" -webpack@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.2.1.tgz#7bb1d72ae2087dd1a4af526afec15eed17dda475" +webpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.4.1.tgz#15a91dbe34966d8a4b99c7d656efd92a2e5a6f6a" dependencies: - acorn "^4.0.4" + acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^4.7.0" ajv-keywords "^1.1.1" @@ -6563,6 +6962,7 @@ webpack@^2.2.1: enhanced-resolve "^3.0.0" interpret "^1.0.0" json-loader "^0.5.4" + json5 "^0.5.1" loader-runner "^2.3.0" loader-utils "^0.2.16" memory-fs "~0.4.1" @@ -6571,11 +6971,21 @@ webpack@^2.2.1: source-map "^0.5.3" supports-color "^3.1.0" tapable "~0.2.5" - uglify-js "^2.7.5" - watchpack "^1.2.0" - webpack-sources "^0.1.4" + uglify-js "^2.8.5" + watchpack "^1.3.1" + webpack-sources "^0.2.3" yargs "^6.0.0" +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + websocket.js@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/websocket.js/-/websocket.js-0.1.7.tgz#8d24cefb1a080c259e7e4740c02cab8f142df2b0" @@ -6680,7 +7090,7 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" -xtend@4.0.1, xtend@^4.0.0, xtend@~4.0.0: +xtend@4.0.1, xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"