From 049cea30b0f3900484d45b0d88a0f073ad0c6cc6 Mon Sep 17 00:00:00 2001 From: Naoki Kosaka Date: Fri, 30 Jun 2017 12:37:17 +0900 Subject: Fix media-gallery, overflow is hidden. (#4008) --- app/javascript/styles/components.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 88431fc69..4df4b0685 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3433,6 +3433,7 @@ button.icon-button.active i.fa-retweet { &, img { width: 100%; + overflow: hidden; } } -- cgit From 0e09048537fee9906ab582bc0704e1a3395c04ed Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 30 Jun 2017 20:40:00 +0900 Subject: Fix broken style in media gallery (regression from #3963) (#4014) --- app/javascript/styles/components.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 4df4b0685..28cb9ec65 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3421,19 +3421,16 @@ button.icon-button.active i.fa-retweet { } .media-gallery__item-thumbnail { - background-position: center; - background-repeat: no-repeat; - background-size: cover; cursor: zoom-in; - display: flex; - align-items: center; + display: block; text-decoration: none; height: 100%; &, img { width: 100%; - overflow: hidden; + height: 100%; + object-fit: cover; } } -- cgit From bf50e3e5aefc88f7a6d9ab4aafe5beab4360292b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Jul 2017 14:50:10 +0200 Subject: Fix height issue in report modal --- app/javascript/styles/components.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 28cb9ec65..a87aa5d79 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3278,6 +3278,7 @@ button.icon-button.active i.fa-retweet { .report-modal__statuses { min-height: 20vh; + max-height: 40vh; overflow-y: auto; overflow-x: hidden; } -- cgit From e7c0d87d984108a5c56c08285e8496a23fca28b8 Mon Sep 17 00:00:00 2001 From: Shin Kojima Date: Thu, 6 Jul 2017 22:27:02 +0900 Subject: Fix embedded SVG fill attribute (#4086) * Fix embedded SVG fill attribute SCSS darken/lighten functions may not return a color value, but a color name like "white". See following example: https://www.sassmeister.com/gist/c41da93b87d536890ddf30a1f42e7816 This patch will normalize $color argument to FFFFFF style. I also changed the function name from "url-friendly-colour" to "hex-color", Because... 1. The name "url-friendly" is not meaningful enough to describe what it does. 2. It is familier to me using "color" rather than "colour" kojima:kojiMac mastodon[master]$ git grep -l colour app/javascript/styles/boost.scss spec/fixtures/files/attachment.jpg kojima:kojiMac mastodon[master]$ git grep -l color .rspec .scss-lint.yml Gemfile.lock app/javascript/mastodon/features/status/components/action_bar.js app/javascript/styles/about.scss app/javascript/styles/accounts.scss app/javascript/styles/admin.scss app/javascript/styles/basics.scss app/javascript/styles/boost.scss app/javascript/styles/compact_header.scss app/javascript/styles/components.scss app/javascript/styles/containers.scss app/javascript/styles/footer.scss app/javascript/styles/forms.scss app/javascript/styles/landing_strip.scss app/javascript/styles/reset.scss app/javascript/styles/stream_entries.scss app/javascript/styles/tables.scss app/javascript/styles/variables.scss app/views/admin/subscriptions/_subscription.html.haml app/views/layouts/application.html.haml app/views/layouts/error.html.haml app/views/manifests/show.json.rabl bin/webpack-dev-server config/initializers/httplog.rb public/500.html public/emoji/1f1e6-1f1e8.svg public/emoji/1f1ec-1f1f8.svg public/emoji/1f1f3-1f1ee.svg public/emoji/1f1fb-1f1ec.svg spec/fixtures/requests/idn.txt yarn.lock * Add semicolon --- app/javascript/styles/boost.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss index 7589828c6..8d6478e10 100644 --- a/app/javascript/styles/boost.scss +++ b/app/javascript/styles/boost.scss @@ -1,11 +1,14 @@ -@function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) +@function hex-color($color) { + @if type-of($color) == 'color' { + $color: str-slice(ie-hex-str($color), 4); + } + @return '%23' + unquote($color) } button.icon-button i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); &:hover { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); } } -- cgit From 34c8a46d7de2b7e0b495b5c4a51c95c6a6b047b7 Mon Sep 17 00:00:00 2001 From: Mantas Date: Thu, 6 Jul 2017 22:26:07 +0300 Subject: Remove ugly blue highlight on Android browsers (#4031) --- app/javascript/styles/basics.scss | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 70a5be367..0cb271ddd 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -11,6 +11,8 @@ body { text-rendering: optimizelegibility; font-feature-settings: "kern"; text-size-adjust: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: transparent; &.app-body { position: fixed; -- cgit From f76e71825da3b185e549cd3267813fd12cee4050 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 7 Jul 2017 04:31:03 +0900 Subject: Improve Activity stream spoiler (#4088) --- app/javascript/packs/public.js | 8 ++++++-- app/javascript/styles/stream_entries.scss | 12 ++++++++++++ app/views/stream_entries/_content_spoiler.html.haml | 10 +++++++--- app/views/stream_entries/_detailed_status.html.haml | 6 ++---- app/views/stream_entries/_simple_status.html.haml | 3 +-- 5 files changed, 28 insertions(+), 11 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index a0e511b0a..254250a3b 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -53,8 +53,12 @@ function main() { } }); - delegate(document, '.media-spoiler', 'click', ({ target }) => { - target.style.display = 'none'; + delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { + this.parentNode.classList.add('media-spoiler-wrapper__visible'); + }); + + delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { + this.parentNode.classList.remove('media-spoiler-wrapper__visible'); }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index fcec32d44..e89cc3f09 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -330,6 +330,18 @@ } } + .media-spoiler-wrapper { + &.media-spoiler-wrapper__visible { + .media-spoiler { + display: none; + } + + .spoiler-button { + display: block; + } + } + } + .pre-header { padding: 14px 0; padding-left: (48px + 14px * 2); diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml index d80ea46a0..798dfce67 100644 --- a/app/views/stream_entries/_content_spoiler.html.haml +++ b/app/views/stream_entries/_content_spoiler.html.haml @@ -1,3 +1,7 @@ -.media-spoiler - %span= t('stream_entries.sensitive_content') - %span= t('stream_entries.click_to_show') +.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' } + .spoiler-button + .icon-button.overlayed + %i.fa.fa-fw.fa-eye + .media-spoiler + %span= t('stream_entries.sensitive_content') + %span= t('stream_entries.click_to_show') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index ef60b9925..193cc6470 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -17,13 +17,11 @@ - unless status.media_attachments.empty? - if status.media_attachments.first.video? .video-player - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } %video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true } - else .detailed-status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } .status__attachments__inner - status.media_attachments.each do |media| = render partial: 'stream_entries/media', locals: { media: media } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index db4e30fda..2df0cc850 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,8 +22,7 @@ - unless status.media_attachments.empty? .status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } - if status.media_attachments.first.video? .status__attachments__inner .video-item -- cgit From 18d3fa953b5af8ab17cc93c33cb95cec37127712 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Thu, 6 Jul 2017 13:39:56 -0700 Subject: Add a setting allowing the use of system's default font in Web UI (#4033) * add a system_font_ui setting on the server * Plug the system_font_ui on the front-end * add EN/FR locales for the new setting * put Roboto after all other fonts * remove trailing whitespace so CodeClimate is happy * fix user_spec.rb * correctly write user_spect this time * slightly better way of adding the classes * add comments to the system-font stack for clarification * use .system-font for the class instead * don't use multiple lines for comments * remove trailing whitespace * use the classnames module for consistency * use `mastodon-font-sans-serif` instead of Roboto directly --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/features/ui/index.js | 14 ++++++++++++-- app/javascript/styles/basics.scss | 15 +++++++++++++++ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/views/home/initial_state.json.rabl | 1 + app/views/settings/preferences/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/locales/simple_form.fr.yml | 1 + config/settings.yml | 1 + spec/lib/user_settings_decorator_spec.rb | 7 +++++++ spec/models/user_spec.rb | 8 ++++++++ 12 files changed, 57 insertions(+), 2 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 71f5a7c04..a15c26031 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -37,6 +37,7 @@ class Settings::PreferencesController < ApplicationController :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_system_font_ui, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 08d087da1..54e623d99 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import Switch from 'react-router-dom/Switch'; import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; @@ -72,12 +73,17 @@ class WrappedRoute extends React.Component { } -@connect() +const mapStateToProps = state => ({ + systemFontUi: state.getIn(['meta', 'system_font_ui']), +}); + +@connect(mapStateToProps) export default class UI extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, + systemFontUi: PropTypes.bool, }; state = { @@ -176,8 +182,12 @@ export default class UI extends React.PureComponent { const { width, draggingOver } = this.state; const { children } = this.props; + const className = classNames('ui', { + 'system-font': this.props.systemFontUi, + }); + return ( -
+
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 0cb271ddd..4da698e81 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -63,3 +63,18 @@ button { align-items: center; justify-content: center; } + +.system-font { + // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+) + // -apple-system => Safari <11 specific + // BlinkMacSystemFont => Chrome <56 on macOS specific + // Segoe UI => Windows 7/8/10 + // Oxygen => KDE + // Ubuntu => Unity/Ubuntu + // Cantarell => GNOME + // Fira Sans => Firefox OS + // Droid Sans => Older Androids (<4.0) + // Helvetica Neue => Older macOS <10.11 + // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0) + font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif; +} diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index af264bbd5..9c0cb4545 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -21,6 +21,7 @@ class UserSettingsDecorator user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference + user.settings['system_font_ui'] = system_font_ui_preference end def merged_notification_emails @@ -43,6 +44,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_delete_modal' end + def system_font_ui_preference + boolean_cast_setting 'setting_system_font_ui' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/models/user.rb b/app/models/user.rb index c31a0c644..e2bb3d0ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -91,6 +91,10 @@ class User < ApplicationRecord settings.auto_play_gif end + def setting_system_font_ui + settings.system_font_ui + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index e305f8e7a..291ff806b 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -11,6 +11,7 @@ node(:meta) do boost_modal: current_account.user.setting_boost_modal, delete_modal: current_account.user.setting_delete_modal, auto_play_gif: current_account.user.setting_auto_play_gif, + system_font_ui: current_account.user.setting_system_font_ui, } end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 721ce6a21..26fbfdf82 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -44,6 +44,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 3e769fb96..d8d3b8a6f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,6 +38,7 @@ en: setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy setting_delete_modal: Show confirmation dialog before deleting a toot + setting_system_font_ui: Use system's default font severity: Severity type: Import type username: Username diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index ae4975143..446c56947 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -28,6 +28,7 @@ fr: password: Mot de passe setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité type: Type d'import username: Identifiant diff --git a/config/settings.yml b/config/settings.yml index 5aea232e7..18b70b51f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -19,6 +19,7 @@ defaults: &defaults boost_modal: false auto_play_gif: true delete_modal: true + system_font_ui: false notification_emails: follow: false reblog: false diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index 66e42fa0e..e1ba56d97 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -48,5 +48,12 @@ describe UserSettingsDecorator do settings.update(values) expect(user.settings['auto_play_gif']).to eq false end + + it 'updates the user settings value for system font in UI' do + values = { 'setting_system_font_ui' => '0' } + + settings.update(values) + expect(user.settings['system_font_ui']).to eq false + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a6df3fb26..2019ec0f6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -184,6 +184,14 @@ RSpec.describe User, type: :model do expect(user.setting_auto_play_gif).to eq false end end + + describe '#setting_system_font_ui' do + it 'returns system font ui setting' do + user = Fabricate(:user) + user.settings[:system_font_ui] = false + expect(user.setting_system_font_ui).to eq false + end + end describe '#setting_boost_modal' do it 'returns boost modal setting' do -- cgit From 348d6f5e7551e632e7dea41e61c40f79aac59be9 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sat, 8 Jul 2017 00:06:02 +0200 Subject: Lazy load components (#3879) * feat: Lazy-load routes * feat: Lazy-load modals * feat: Lazy-load columns * refactor: Simplify Bundle API * feat: Optimize bundles * feat: Prevent flashing the waiting state * feat: Preload commonly used bundles * feat: Lazy load Compose reducers * feat: Lazy load Notifications reducer * refactor: Move all dynamic imports into one file * fix: Minor bugs * fix: Manually hydrate the lazy-loaded reducers * refactor: Move all dynamic imports to async-components * fix: Loading modal style * refactor: Avoid converting the raw state for each lazy hydration * refactor: Remove unused component * refactor: Maintain modal name * fix: Add as=script to preload link * chore: Fix lint error * fix(components/bundle): Check if timestamp is set when computing elapsed * fix: Load compose reducers for the onboarding modal --- app/javascript/mastodon/actions/bundles.js | 25 ++++ app/javascript/mastodon/actions/store.js | 8 ++ app/javascript/mastodon/components/status.js | 27 +++- app/javascript/mastodon/containers/mastodon.js | 5 +- .../compose/components/emoji_picker_dropdown.js | 3 +- .../mastodon/features/ui/components/bundle.js | 96 ++++++++++++++ .../features/ui/components/bundle_column_error.js | 44 +++++++ .../features/ui/components/bundle_modal_error.js | 53 ++++++++ .../features/ui/components/column_loading.js | 13 ++ .../features/ui/components/columns_area.js | 28 ++-- .../features/ui/components/modal_loading.js | 20 +++ .../mastodon/features/ui/components/modal_root.js | 51 +++++--- .../features/ui/containers/bundle_container.js | 19 +++ app/javascript/mastodon/features/ui/index.js | 89 +++++-------- .../mastodon/features/ui/util/async-components.js | 143 +++++++++++++++++++++ .../features/ui/util/react_router_helpers.js | 65 ++++++++++ app/javascript/mastodon/reducers/compose.js | 4 +- app/javascript/mastodon/reducers/index.js | 21 +-- .../mastodon/reducers/media_attachments.js | 4 +- app/javascript/mastodon/store/configureStore.js | 25 +++- app/javascript/styles/components.scss | 31 ++++- app/views/layouts/application.html.haml | 17 +++ 22 files changed, 680 insertions(+), 111 deletions(-) create mode 100644 app/javascript/mastodon/actions/bundles.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_modal_error.js create mode 100644 app/javascript/mastodon/features/ui/components/column_loading.js create mode 100644 app/javascript/mastodon/features/ui/components/modal_loading.js create mode 100644 app/javascript/mastodon/features/ui/containers/bundle_container.js create mode 100644 app/javascript/mastodon/features/ui/util/async-components.js create mode 100644 app/javascript/mastodon/features/ui/util/react_router_helpers.js (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js new file mode 100644 index 000000000..ecc9c8f7d --- /dev/null +++ b/app/javascript/mastodon/actions/bundles.js @@ -0,0 +1,25 @@ +export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; +export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; +export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; + +export function fetchBundleRequest(skipLoading) { + return { + type: BUNDLE_FETCH_REQUEST, + skipLoading, + }; +} + +export function fetchBundleSuccess(skipLoading) { + return { + type: BUNDLE_FETCH_SUCCESS, + skipLoading, + }; +} + +export function fetchBundleFail(error, skipLoading) { + return { + type: BUNDLE_FETCH_FAIL, + error, + skipLoading, + }; +} diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 601cea001..08c2810ca 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,6 +1,7 @@ import Immutable from 'immutable'; export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => Immutable.fromJS(rawState, (k, v) => @@ -15,3 +16,10 @@ export function hydrateStore(rawState) { state, }; }; + +export function hydrateStoreLazy(name, state) { + return { + type: `${STORE_HYDRATE_LAZY}-${name}`, + state, + }; +}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index ff574ab3d..18ce0198e 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -5,8 +5,6 @@ import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; -import MediaGallery from './media_gallery'; -import VideoPlayer from './video_player'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; @@ -14,6 +12,11 @@ import emojify from '../emoji'; import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; + +// We use the component (and not the container) since we do not want +// to use the progress bar to show download progress +import Bundle from '../features/ui/components/bundle'; export default class Status extends ImmutablePureComponent { @@ -154,6 +157,14 @@ export default class Status extends ImmutablePureComponent { this.setState({ isExpanded: !this.state.isExpanded }); }; + renderLoadingMediaGallery () { + return
; + } + + renderLoadingVideoPlayer () { + return
; + } + render () { let media = null; let statusAvatar; @@ -201,9 +212,17 @@ export default class Status extends ImmutablePureComponent { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + media = ( + + {Component => } + + ); } else { - media = ; + media = ( + + {Component => } + + ); } } diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 3bd89902f..6e79f9e4f 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -22,9 +22,10 @@ import { getLocale } from '../locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); +export const store = configureStore(); const initialState = JSON.parse(document.getElementById('initial-state').textContent); -store.dispatch(hydrateStore(initialState)); +export const hydrateAction = hydrateStore(initialState); +store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c83dbb63e..83c66a5d5 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.setState({ active: true }); if (!EmojiPicker) { this.setState({ loading: true }); - import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => { + EmojiPickerAsync().then(TheEmojiPicker => { EmojiPicker = TheEmojiPicker.default; this.setState({ loading: false }); }).catch(() => { diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js new file mode 100644 index 000000000..e69a32f47 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const emptyComponent = () => null; +const noop = () => { }; + +class Bundle extends React.Component { + + static propTypes = { + fetchComponent: PropTypes.func.isRequired, + loading: PropTypes.func, + error: PropTypes.func, + children: PropTypes.func.isRequired, + renderDelay: PropTypes.number, + onRender: PropTypes.func, + onFetch: PropTypes.func, + onFetchSuccess: PropTypes.func, + onFetchFail: PropTypes.func, + } + + static defaultProps = { + loading: emptyComponent, + error: emptyComponent, + renderDelay: 0, + onRender: noop, + onFetch: noop, + onFetchSuccess: noop, + onFetchFail: noop, + } + + state = { + mod: undefined, + forceRender: false, + } + + componentWillMount() { + this.load(this.props); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.fetchComponent !== this.props.fetchComponent) { + this.load(nextProps); + } + } + + componentDidUpdate () { + this.props.onRender(); + } + + componentWillUnmount () { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + + load = (props) => { + const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; + + this.setState({ mod: undefined }); + onFetch(); + + if (renderDelay !== 0) { + this.timestamp = new Date(); + this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); + } + + return fetchComponent() + .then((mod) => { + this.setState({ mod: mod.default }); + onFetchSuccess(); + }) + .catch((error) => { + this.setState({ mod: null }); + onFetchFail(error); + }); + } + + render() { + const { loading: Loading, error: Error, children, renderDelay } = this.props; + const { mod, forceRender } = this.state; + const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; + + if (mod === undefined) { + return (elapsed >= renderDelay || forceRender) ? : null; + } + + if (mod === null) { + return ; + } + + return children(mod); + } + +} + +export default Bundle; diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js new file mode 100644 index 000000000..cd124746a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import Column from './column'; +import ColumnHeader from './column_header'; +import ColumnBackButtonSlim from '../../../components/column_back_button_slim'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, + body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, +}); + +class BundleColumnError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { intl: { formatMessage } } = this.props; + + return ( + + + +
+ + {formatMessage(messages.body)} +
+
+ ); + } + +} + +export default injectIntl(BundleColumnError); diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js new file mode 100644 index 000000000..928bfe1f7 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, + close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, +}); + +class BundleModalError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { onClose, intl: { formatMessage } } = this.props; + + // Keep the markup in sync with + // (make sure they have the same dimensions) + return ( +
+
+ + {formatMessage(messages.error)} +
+ +
+
+ +
+
+
+ ); + } + +} + +export default injectIntl(BundleModalError); diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js new file mode 100644 index 000000000..9bb9c14a1 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; + +const ColumnLoading = () => ( + + +
+ +); + +export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 01167b6e5..5fa27599f 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -2,15 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + import ReactSwipeable from 'react-swipeable'; -import HomeTimeline from '../../home_timeline'; -import Notifications from '../../notifications'; -import PublicTimeline from '../../public_timeline'; -import CommunityTimeline from '../../community_timeline'; -import HashtagTimeline from '../../hashtag_timeline'; -import Compose from '../../compose'; import { getPreviousLink, getNextLink } from './tabs_bar'; +import BundleContainer from '../containers/bundle_container'; +import ColumnLoading from './column_loading'; +import BundleColumnError from './bundle_column_error'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components'; + const componentMap = { 'COMPOSE': Compose, 'HOME': HomeTimeline, @@ -48,6 +48,14 @@ export default class ColumnsArea extends ImmutablePureComponent { } }; + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + render () { const { columns, children, singleColumn } = this.props; @@ -62,9 +70,13 @@ export default class ColumnsArea extends ImmutablePureComponent { return (
{columns.map(column => { - const SpecificComponent = componentMap[column.get('id')]; const params = column.get('params', null) === null ? null : column.get('params').toJS(); - return ; + + return ( + + {SpecificComponent => } + + ); })} {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js new file mode 100644 index 000000000..f403ca4c9 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/modal_loading.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import LoadingIndicator from '../../../components/loading_indicator'; + +// Keep the markup in sync with +// (make sure they have the same dimensions) +const ModalLoading = () => ( +
+
+ +
+
+
+
+
+
+); + +export default ModalLoading; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 48b048eb7..085299038 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,13 +1,18 @@ 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 ReportModal from './report_modal'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import BundleContainer from '../containers/bundle_container'; +import BundleModalError from './bundle_modal_error'; +import ModalLoading from './modal_loading'; +import { + MediaModal, + OnboardingModal, + VideoModal, + BoostModal, + ConfirmationModal, + ReportModal, +} from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, @@ -49,6 +54,22 @@ export default class ModalRoot extends React.PureComponent { return { opacity: spring(0), scale: spring(0.98) }; } + renderModal = (SpecificComponent) => { + const { props, onClose } = this.props; + + return ; + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + const { onClose } = this.props; + + return ; + } + render () { const { type, props, onClose } = this.props; const visible = !!type; @@ -70,18 +91,14 @@ export default class ModalRoot extends React.PureComponent { > {interpolatedStyles =>
- {interpolatedStyles.map(({ key, data: { type, props }, style }) => { - const SpecificComponent = MODAL_COMPONENTS[type]; - - return ( -
-
-
- -
+ {interpolatedStyles.map(({ key, data: { type }, style }) => ( +
+
+
+ {this.renderModal}
- ); - })} +
+ ))}
} diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js new file mode 100644 index 000000000..7e3f0c3a6 --- /dev/null +++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; + +import Bundle from '../components/bundle'; + +import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; + +const mapDispatchToProps = dispatch => ({ + onFetch () { + dispatch(fetchBundleRequest()); + }, + onFetchSuccess () { + dispatch(fetchBundleSuccess()); + }, + onFetchFail (error) { + dispatch(fetchBundleFail(error)); + }, +}); + +export default connect(null, mapDispatchToProps)(Bundle); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 54e623d99..6057d8797 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,7 +1,5 @@ import React from 'react'; import classNames from 'classnames'; -import Switch from 'react-router-dom/Switch'; -import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; import NotificationsContainer from './containers/notifications_container'; import PropTypes from 'prop-types'; @@ -14,64 +12,40 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; +import { store } from '../../containers/mastodon'; import ColumnsAreaContainer from './containers/columns_area_container'; -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 AccountGallery from '../../features/account_gallery'; -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'; - -// Small wrapper to pass multiColumn to the route components -const WrappedSwitch = ({ multiColumn, children }) => ( - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - -); - -WrappedSwitch.propTypes = { - multiColumn: PropTypes.bool, - children: PropTypes.node, -}; - -// Small Wraper to extract the params from the route and pass -// them to the rendered component, together with the content to -// be rendered inside (the children) -class WrappedRoute extends React.Component { - - static propTypes = { - component: PropTypes.func.isRequired, - content: PropTypes.node, - multiColumn: PropTypes.bool, - } - - renderComponent = ({ match: { params } }) => { - const { component: Component, content, multiColumn } = this.props; - - return {content}; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return ; - } +import { + Compose, + Status, + GettingStarted, + PublicTimeline, + CommunityTimeline, + AccountTimeline, + AccountGallery, + HomeTimeline, + Followers, + Following, + Reblogs, + Favourites, + HashtagTimeline, + Notifications as AsyncNotifications, + FollowRequests, + GenericNotFound, + FavouritedStatuses, + Blocks, + Mutes, +} from './util/async-components'; + +const Notifications = () => AsyncNotifications().then(component => { + store.dispatch(refreshNotifications()); + return component; +}); -} +// Dummy import, to make sure that ends up in the application bundle. +// Without this it ends up in ~8 very commonly used bundles. +import '../../components/status'; const mapStateToProps = state => ({ systemFontUi: state.getIn(['meta', 'system_font_ui']), @@ -162,7 +136,6 @@ export default class UI extends React.PureComponent { document.addEventListener('dragend', this.handleDragEnd, false); this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); } componentWillUnmount () { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js new file mode 100644 index 000000000..c9f81136d --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -0,0 +1,143 @@ +import { store } from '../../../containers/mastodon'; +import { injectAsyncReducer } from '../../../store/configureStore'; + +// NOTE: When lazy-loading reducers, make sure to add them +// to application.html.haml (if the component is preloaded there) + +export function EmojiPicker () { + return import(/* webpackChunkName: "emojione_picker" */'emojione-picker'); +} + +export function Compose () { + return Promise.all([ + import(/* webpackChunkName: "features/compose" */'../../compose'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'), + ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + injectAsyncReducer(store, 'search', searchReducer.default); + + return component; + }); +} + +export function Notifications () { + return Promise.all([ + import(/* webpackChunkName: "features/notifications" */'../../notifications'), + import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'), + ]).then(([component, notificationsReducer]) => { + injectAsyncReducer(store, 'notifications', notificationsReducer.default); + + return component; + }); +} + +export function HomeTimeline () { + return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline'); +} + +export function PublicTimeline () { + return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); +} + +export function CommunityTimeline () { + return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); +} + +export function HashtagTimeline () { + return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); +} + +export function Status () { + return import(/* webpackChunkName: "features/status" */'../../status'); +} + +export function GettingStarted () { + return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); +} + +export function AccountTimeline () { + return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); +} + +export function AccountGallery () { + return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery'); +} + +export function Followers () { + return import(/* webpackChunkName: "features/followers" */'../../followers'); +} + +export function Following () { + return import(/* webpackChunkName: "features/following" */'../../following'); +} + +export function Reblogs () { + return import(/* webpackChunkName: "features/reblogs" */'../../reblogs'); +} + +export function Favourites () { + return import(/* webpackChunkName: "features/favourites" */'../../favourites'); +} + +export function FollowRequests () { + return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); +} + +export function GenericNotFound () { + return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found'); +} + +export function FavouritedStatuses () { + return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); +} + +export function Blocks () { + return import(/* webpackChunkName: "features/blocks" */'../../blocks'); +} + +export function Mutes () { + return import(/* webpackChunkName: "features/mutes" */'../../mutes'); +} + +export function MediaModal () { + return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal'); +} + +export function OnboardingModal () { + return Promise.all([ + import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + ]).then(([component, composeReducer, mediaAttachmentsReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + return component; + }); +} + +export function VideoModal () { + return import(/* webpackChunkName: "modals/video_modal" */'../components/video_modal'); +} + +export function BoostModal () { + return import(/* webpackChunkName: "modals/boost_modal" */'../components/boost_modal'); +} + +export function ConfirmationModal () { + return import(/* webpackChunkName: "modals/confirmation_modal" */'../components/confirmation_modal'); +} + +export function ReportModal () { + return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); +} + +export function MediaGallery () { + return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); +} + +export function VideoPlayer () { + return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); +} diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js new file mode 100644 index 000000000..e33a6df6f --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Switch from 'react-router-dom/Switch'; +import Route from 'react-router-dom/Route'; + +import ColumnLoading from '../components/column_loading'; +import BundleColumnError from '../components/bundle_column_error'; +import BundleContainer from '../containers/bundle_container'; + +// Small wrapper to pass multiColumn to the route components +export const WrappedSwitch = ({ multiColumn, children }) => ( + + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + +); + +WrappedSwitch.propTypes = { + multiColumn: PropTypes.bool, + children: PropTypes.node, +}; + +// Small Wraper to extract the params from the route and pass +// them to the rendered component, together with the content to +// be rendered inside (the children) +export class WrappedRoute extends React.Component { + + static propTypes = { + component: PropTypes.func.isRequired, + content: PropTypes.node, + multiColumn: PropTypes.bool, + } + + renderComponent = ({ match }) => { + this.match = match; // Needed for this.renderBundle + + const { component } = this.props; + + return ( + + {this.renderBundle} + + ); + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + + renderBundle = (Component) => { + const { match: { params }, props: { content, multiColumn } } = this; + + return {content}; + } + + render () { + const { component: Component, content, ...rest } = this.props; + + return ; + } + +} diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index d0b47a85c..09db95e2d 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -23,7 +23,7 @@ import { COMPOSE_EMOJI_INSERT, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; import uuid from '../uuid'; @@ -134,7 +134,7 @@ const privacyPreference = (a, b) => { export default function compose(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-compose`: return clearAll(state.merge(action.state.get('compose'))); case COMPOSE_MOUNT: return state.set('mounted', true); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index be402a16b..79062f2f9 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -1,7 +1,6 @@ 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'; @@ -9,20 +8,16 @@ import user_lists from './user_lists'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; import statuses from './statuses'; -import media_attachments from './media_attachments'; 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'; import contexts from './contexts'; -export default combineReducers({ +const reducers = { timelines, meta, - compose, alerts, loadingBar: loadingBarReducer, modal, @@ -30,13 +25,19 @@ export default combineReducers({ status_lists, accounts, accounts_counters, - media_attachments, statuses, relationships, - search, - notifications, settings, cards, reports, contexts, -}); +}; + +export function createReducer(asyncReducers) { + return combineReducers({ + ...reducers, + ...asyncReducers, + }); +} + +export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index 85bea4f0b..d17d465aa 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,4 +1,4 @@ -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -7,7 +7,7 @@ const initialState = Immutable.Map({ export default function meta(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-media_attachments`: return state.merge(action.state.get('media_attachments')); default: return state; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 1376d4cba..0fe29f031 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,15 +1,36 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import appReducer from '../reducers'; +import appReducer, { createReducer } from '../reducers'; +import { hydrateStoreLazy } from '../actions/store'; +import { hydrateAction } from '../containers/mastodon'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( + const store = createStore(appReducer, compose(applyMiddleware( thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware() ), window.devToolsExtension ? window.devToolsExtension() : f => f)); + + store.asyncReducers = { }; + + return store; }; + +export function injectAsyncReducer(store, name, asyncReducer) { + if (!store.asyncReducers[name]) { + // Keep track that we injected this reducer + store.asyncReducers[name] = asyncReducer; + + // Add the current reducer to the store + store.replaceReducer(createReducer(store.asyncReducers)); + + // The state this reducer handles defaults to its initial state (stored inside the reducer) + // But that state may be out of date because of the server-side hydration, so we replay + // the hydration action but only for this reducer (all async reducers must listen for this dynamic action) + store.dispatch(hydrateStoreLazy(name, hydrateAction.state)); + } +} diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index a87aa5d79..9b500c7ad 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2300,7 +2300,8 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } -.empty-column-indicator { +.empty-column-indicator, +.error-column { color: lighten($ui-base-color, 20%); background: $ui-base-color; text-align: center; @@ -2326,6 +2327,10 @@ button.icon-button.active i.fa-retweet { } } +.error-column { + flex-direction: column; +} + @keyframes pulse { 0% { opacity: 1; @@ -2909,7 +2914,8 @@ button.icon-button.active i.fa-retweet { z-index: 100; } -.onboarding-modal { +.onboarding-modal, +.error-modal { background: $ui-secondary-color; color: $ui-base-color; border-radius: 8px; @@ -2918,7 +2924,8 @@ button.icon-button.active i.fa-retweet { flex-direction: column; } -.onboarding-modal__pager { +.onboarding-modal__pager, +.error-modal__body { height: 80vh; width: 80vw; max-width: 520px; @@ -2943,6 +2950,14 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + @media screen and (max-width: 550px) { .onboarding-modal { width: 100%; @@ -2959,7 +2974,8 @@ button.icon-button.active i.fa-retweet { } } -.onboarding-modal__paginator { +.onboarding-modal__paginator, +.error-modal__footer { flex: 0 0 auto; background: darken($ui-secondary-color, 8%); display: flex; @@ -2969,7 +2985,8 @@ button.icon-button.active i.fa-retweet { min-width: 33px; } - .onboarding-modal__nav { + .onboarding-modal__nav, + .error-modal__nav { color: darken($ui-secondary-color, 34%); background-color: transparent; border: 0; @@ -2992,6 +3009,10 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__footer { + justify-content: center; +} + .onboarding-modal__dots { flex: 1 1 auto; display: flex; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f991bc74f..68d346859 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -20,6 +20,23 @@ = stylesheet_pack_tag 'application', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + + = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/media_attachments', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/search', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags -- cgit From 76318f88306656a4a74f190375a78713327bc4e4 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Sun, 9 Jul 2017 00:21:59 +0900 Subject: Don't use preview when image size is unknown (#4113) --- .../mastodon/features/ui/components/image_loader.js | 16 ++++++++++++---- .../mastodon/features/ui/components/media_modal.js | 5 ++++- app/javascript/styles/components.scss | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 52c3a898b..5ea55d1d2 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent { alt: PropTypes.string, src: PropTypes.string.isRequired, previewSrc: PropTypes.string.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, + width: PropTypes.number, + height: PropTypes.number, } static defaultProps = { alt: '', + width: null, + height: null, }; state = { @@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent { this.setState({ loading: true, error: false }); Promise.all([ this.loadPreviewCanvas(props), - this.loadOriginalImage(props), - ]) + this.hasSize() && this.loadOriginalImage(props), + ].filter(Boolean)) .then(() => { this.setState({ loading: false, error: false }); this.clearPreviewCanvas(); @@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent { this.removers = []; } + hasSize () { + const { width, height } = this.props; + return typeof width === 'number' && typeof height === 'number'; + } + setCanvasRef = c => { this.canvas = c; } @@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent { const className = classNames('image-loader', { 'image-loader--loading': loading, + 'image-loader--amorphous': !this.hasSize(), }); return ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 8bb81ca01..a5b9dc19f 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -74,7 +74,10 @@ export default class MediaModal extends ImmutablePureComponent { } if (attachment.get('type') === 'image') { - content = ; + const width = attachment.getIn(['meta', 'original', 'width']) || null; + const height = attachment.getIn(['meta', 'original', 'height']) || null; + + content = ; } else if (attachment.get('type') === 'gifv') { content = ; } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 9b500c7ad..66d2715da 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1117,6 +1117,20 @@ height: 100%; background-image: none; } + + &.image-loader--amorphous { + position: static; + + .image-loader__preview-canvas { + display: none; + } + + .image-loader__img { + position: static; + width: auto; + height: auto; + } + } } .navigation-bar { -- cgit From fc4c74660b690038ae48264f9d5b0230df58acc4 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 15:02:26 +0200 Subject: Swipeable views (#4105) * feat: Replace react-swipeable with react-swipeable-views * fix: iOS 9 --- .../mastodon/components/column_header.js | 2 +- .../features/ui/components/column_loading.js | 10 ++++- .../features/ui/components/columns_area.js | 48 +++++++++++++--------- .../mastodon/features/ui/components/media_modal.js | 18 +++++--- .../features/ui/components/onboarding_modal.js | 40 +++++++----------- .../mastodon/features/ui/components/tabs_bar.js | 26 ++++-------- app/javascript/styles/components.scss | 40 +++++++++++++++++- package.json | 2 +- yarn.lock | 46 +++++++++++++++++---- 9 files changed, 150 insertions(+), 82 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index ec9379320..5b2a4d84c 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent { }; static propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, icon: PropTypes.string.isRequired, active: PropTypes.bool, multiColumn: PropTypes.bool, diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 9bb9c14a1..7ecfaf77a 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -1,13 +1,19 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Column from '../../../components/column'; import ColumnHeader from '../../../components/column_header'; -const ColumnLoading = () => ( +const ColumnLoading = ({ title = '', icon = ' ' }) => ( - +
); +ColumnLoading.propTypes = { + title: PropTypes.node, + icon: PropTypes.string, +}; + export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 5fa27599f..9ff913774 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import ReactSwipeable from 'react-swipeable'; -import { getPreviousLink, getNextLink } from './tabs_bar'; +import ReactSwipeableViews from 'react-swipeable-views'; +import { links, getIndex, getLink } from './tabs_bar'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; @@ -32,21 +32,29 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; - handleRightSwipe = () => { - const previousLink = getPreviousLink(this.context.router.history.location.pathname); - - if (previousLink) { - this.context.router.history.push(previousLink); - } + handleSwipe = (index) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + this.context.router.history.push(getLink(index)); + }); + }); } - handleLeftSwipe = () => { - const previousLink = getNextLink(this.context.router.history.location.pathname); + renderView = (link, index) => { + const columnIndex = getIndex(this.context.router.history.location.pathname); + const title = link.props.children[1] && React.cloneElement(link.props.children[1]); + const icon = (link.props.children[0] || link.props.children).props.className.split(' ')[2].split('-')[1]; - if (previousLink) { - this.context.router.history.push(previousLink); - } - }; + const view = (index === columnIndex) ? + React.cloneElement(this.props.children) : + ; + + return ( +
+ {view} +
+ ); + } renderLoading = () => { return ; @@ -59,12 +67,14 @@ export default class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn } = this.props; + const columnIndex = getIndex(this.context.router.history.location.pathname); + if (singleColumn) { - return ( - - {children} - - ); + return columnIndex !== -1 ? ( + + {links.map(this.renderView)} + + ) :
{children}>
; } return ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index a5b9dc19f..769e18820 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ExtendedVideoPlayer from '../../../components/extended_video_player'; @@ -26,6 +26,10 @@ export default class MediaModal extends ImmutablePureComponent { index: null, }; + handleSwipe = (index) => { + this.setState({ index: (index) % this.props.media.size }); + } + handleNextClick = () => { this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); } @@ -74,10 +78,12 @@ export default class MediaModal extends ImmutablePureComponent { } if (attachment.get('type') === 'image') { - const width = attachment.getIn(['meta', 'original', 'width']) || null; - const height = attachment.getIn(['meta', 'original', 'height']) || null; + content = media.map((image) => { + const width = image.getIn(['meta', 'original', 'width']) || null; + const height = image.getIn(['meta', 'original', 'height']) || null; - content = ; + return ; + }).toArray(); } else if (attachment.get('type') === 'gifv') { content = ; } @@ -88,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent {
- + {content} - +
{rightNav} diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index b056357a2..189bd8665 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -3,11 +3,9 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import classNames from 'classnames'; import Permalink from '../../../components/permalink'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; import ComposeForm from '../../compose/components/compose_form'; import Search from '../../compose/components/search'; import NavigationBar from '../../compose/components/navigation_bar'; @@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent { })); } + handleSwipe = (index) => { + this.setState({ currentIndex: index }); + } + handleKeyUp = ({ key }) => { switch (key) { case 'ArrowLeft': @@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent { ); - const styles = pages.map((data, i) => ({ - key: `page-${i}`, - data, - style: { - opacity: spring(i === currentIndex ? 1 : 0), - }, - })); - return (
- - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, data, style }, i) => { - const className = classNames('onboarding-modal__page__wrapper', { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - return ( -
{data}
- ); - })} -
- )} -
+ + {pages.map((page, i) => { + const className = classNames('onboarding-modal__page__wrapper', { + 'onboarding-modal__page__wrapper--active': i === currentIndex, + }); + return ( +
{page}
+ ); + })} +
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index c2e6c88b5..b4153ff45 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import NavLink from 'react-router-dom/NavLink'; import { FormattedMessage } from 'react-intl'; -const links = [ +export const links = [ , , , @@ -13,25 +13,13 @@ const links = [ , ]; -export function getPreviousLink (path) { - const index = links.findIndex(link => link.props.to === path); - - if (index > 0) { - return links[index - 1].props.to; - } - - return null; -}; - -export function getNextLink (path) { - const index = links.findIndex(link => link.props.to === path); - - if (index !== -1 && index < links.length - 1) { - return links[index + 1].props.to; - } +export function getIndex (path) { + return links.findIndex(link => link.props.to === path); +} - return null; -}; +export function getLink (index) { + return links[index].props.to; +} export default class TabsBar extends React.Component { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 66d2715da..397126ec1 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1266,6 +1266,23 @@ .columns-area { padding: 10px; } + + .react-swipeable-view-container .columns-area { + height: calc(100% - 20px) !important; + } +} + +.react-swipeable-view-container { + &, + .columns-area, + .drawer, + .column { + height: 100%; + } +} + +.react-swipeable-view-container > * { + height: 100%; } .column { @@ -2910,7 +2927,7 @@ button.icon-button.active i.fa-retweet { video { max-width: 80vw; max-height: 80vh; - width: auto; + width: 100%; height: auto; } @@ -2938,7 +2955,26 @@ button.icon-button.active i.fa-retweet { flex-direction: column; } -.onboarding-modal__pager, +.onboarding-modal__pager { + height: 80vh; + width: 80vw; + max-width: 520px; + max-height: 420px; + + .react-swipeable-view-container > div { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 25px; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + display: flex; + user-select: text; + } +} + .error-modal__body { height: 80vh; width: 80vw; diff --git a/package.json b/package.json index 2f63d0bbd..94545c47e 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "react-router-dom": "^4.1.1", "react-router-scroll": "ytase/react-router-scroll#build", "react-simple-dropdown": "^3.0.0", - "react-swipeable": "^4.0.1", + "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.0.7", "react-toggle": "^4.0.1", "redis": "^2.7.1", diff --git a/yarn.lock b/yarn.lock index d8ed20343..d4295cb50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1259,7 +1259,7 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" -babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: +babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" dependencies: @@ -2318,7 +2318,7 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" -"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0: +"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" @@ -3937,7 +3937,7 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" -keycode@^2.1.8: +keycode@^2.1.7, keycode@^2.1.8: version "2.1.9" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" @@ -5711,6 +5711,15 @@ react-element-to-jsx-string@^5.0.0: stringify-object "2.4.0" traverse "^0.6.6" +react-event-listener@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.4.5.tgz#e3e895a0970cf14ee8f890113af68197abf3d0b1" + dependencies: + babel-runtime "^6.20.0" + fbjs "^0.8.4" + prop-types "^15.5.4" + warning "^3.0.0" + react-html-attributes@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.3.0.tgz#c97896e9cac47ad9c4e6618b835029a826f5d28c" @@ -5875,11 +5884,34 @@ react-style-proptype@^3.0.0: dependencies: prop-types "^15.5.4" -react-swipeable@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-4.0.1.tgz#2cb3a04a52ccebf5361881b30e233dc13ee4b115" +react-swipeable-views-core@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.11.1.tgz#61d046799f90725bbf91a0eb3abcab805c774cac" dependencies: - prop-types "^15.5.8" + babel-runtime "^6.23.0" + warning "^3.0.0" + +react-swipeable-views-utils@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.0.tgz#4ff11f20a8da0561f623876d9fd691116e1a6a03" + dependencies: + babel-runtime "^6.23.0" + fbjs "^0.8.4" + keycode "^2.1.7" + prop-types "^15.5.4" + react-event-listener "^0.4.5" + react-swipeable-views-core "^0.11.1" + +react-swipeable-views@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.3.tgz#b0d3f417bcbcd06afda2f8437c15e8360a568744" + dependencies: + babel-runtime "^6.23.0" + dom-helpers "^3.2.1" + prop-types "^15.5.4" + react-swipeable-views-core "^0.11.1" + react-swipeable-views-utils "^0.12.0" + warning "^3.0.0" react-test-renderer@^15.6.1: version "15.6.1" -- cgit From 4122a837fadf8cf59712b5c1790ac0af96bcbc84 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 18:49:07 +0200 Subject: fix(components/media_modal): Aspect ratio (#4128) * fix(components/media_modal): Aspect ratio * fix: Remove useless style --- app/javascript/mastodon/features/ui/components/image_loader.js | 1 + app/javascript/mastodon/features/ui/components/media_modal.js | 2 +- app/javascript/styles/components.scss | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 5ea55d1d2..aad594380 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -133,6 +133,7 @@ export default class ImageLoader extends React.PureComponent { width={width} height={height} ref={this.setCanvasRef} + style={{ opacity: loading ? 1 : 0 }} /> {!loading && ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 769e18820..23f588669 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -94,7 +94,7 @@ export default class MediaModal extends ImmutablePureComponent {
- + {content}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 397126ec1..97651b5f4 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2935,6 +2935,7 @@ button.icon-button.active i.fa-retweet { canvas { display: block; background: url('../images/void.png') repeat; + object-fit: contain; } } -- cgit From 7a889a8e125a03e109b225cd83b0abcbdc76d95b Mon Sep 17 00:00:00 2001 From: STJrInuyasha Date: Mon, 10 Jul 2017 09:05:06 -0700 Subject: Remote following success page (#4129) * Added a success page to remote following Includes follow-through links to web (the old redirect target) and back to the remote user's profile * Use Account.new in spec instead of a fake with only id (fixes spec) * Fabricate(:account) over Account.new * Remove self from the success text (and all HTML with it) --- app/controllers/authorize_follows_controller.rb | 2 +- app/javascript/styles/forms.scss | 9 +++++++++ app/views/authorize_follows/success.html.haml | 16 ++++++++++++++++ config/locales/en.yml | 6 ++++++ spec/controllers/authorize_follows_controller_spec.rb | 4 ++-- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 app/views/authorize_follows/success.html.haml (limited to 'app/javascript/styles') diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index da4ef022a..dccd1c209 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController if @account.nil? render :error else - redirect_to web_url("accounts/#{@account.id}") + render :success end rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError render :error diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 7a181f36b..414dc4fe8 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -375,3 +375,12 @@ code { width: 50%; } } + +.post-follow-actions { + text-align: center; + color: $ui-primary-color; + + div { + margin-bottom: 4px; + } +} diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml new file mode 100644 index 000000000..f0b495689 --- /dev/null +++ b/app/views/authorize_follows/success.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('authorize_follow.title', acct: @account.acct) + +.form-container + .follow-prompt + - if @account.locked? + %h2= t('authorize_follow.follow_request') + - else + %h2= t('authorize_follow.following') + + = render 'card', account: @account + + .post-follow-actions + %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block' + %div= t('authorize_follow.post_follow.close') diff --git a/config/locales/en.yml b/config/locales/en.yml index 60e192491..8bb893d1c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -221,6 +221,12 @@ en: authorize_follow: error: Unfortunately, there was an error looking up the remote account follow: Follow + following: 'Success! You are now following:' + follow_request: 'You have sent a follow request to:' + post_follow: + web: Go to web + return: Return to the user's profile + close: Or, you can just close this window. prompt_html: 'You (%{self}) have requested to follow:' title: Follow %{acct} datetime: diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb index b801aa661..26e46a23c 100644 --- a/spec/controllers/authorize_follows_controller_spec.rb +++ b/spec/controllers/authorize_follows_controller_spec.rb @@ -94,7 +94,7 @@ describe AuthorizeFollowsController do end it 'follows account when found' do - target_account = double(id: '123') + target_account = Fabricate(:account) result_account = double(target_account: target_account) service = double allow(FollowService).to receive(:new).and_return(service) @@ -103,7 +103,7 @@ describe AuthorizeFollowsController do post :create, params: { acct: 'acct:user@hostname' } expect(service).to have_received(:call).with(account, 'user@hostname') - expect(response).to redirect_to(web_url('accounts/123')) + expect(response).to render_template(:success) end end end -- cgit From 31366334cb186974fe97234e05fe36d19d33841b Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 11 Jul 2017 21:36:27 +0900 Subject: Drawer tab according to column (#4135) * Add notifications link to drawer * Remove local and public timeline tab in drawer * Add home --- app/javascript/mastodon/features/compose/index.js | 20 ++++++++++++++++++-- app/javascript/mastodon/locales/defaultMessages.json | 8 ++++++++ app/javascript/styles/components.scss | 3 +-- 3 files changed, 27 insertions(+), 4 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 747fe4216..6aa5de96c 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ComposeFormContainer from './containers/compose_form_container'; import NavigationContainer from './containers/navigation_container'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; import Link from 'react-router-dom/Link'; @@ -13,6 +14,8 @@ import SearchResultsContainer from './containers/search_results_container'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, 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' }, @@ -20,6 +23,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ + columns: state.getIn(['settings', 'columns']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), }); @@ -29,6 +33,7 @@ export default class Compose extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + columns: ImmutablePropTypes.list.isRequired, multiColumn: PropTypes.bool, showSearch: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -48,11 +53,22 @@ export default class Compose extends React.PureComponent { let header = ''; if (multiColumn) { + const { columns } = this.props; header = (
- - + {!columns.some(column => column.get('id') === 'HOME') && ( + + )} + {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( + + )} + {!columns.some(column => column.get('id') === 'COMMUNITY') && ( + + )} + {!columns.some(column => column.get('id') === 'PUBLIC') && ( + + )}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 88f0f9c30..7c1522299 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -635,6 +635,14 @@ "defaultMessage": "Getting started", "id": "getting_started.heading" }, + { + "defaultMessage": "Home", + "id": "tabs_bar.home" + }, + { + "defaultMessage": "Notifications", + "id": "tabs_bar.notifications" + }, { "defaultMessage": "Federated timeline", "id": "navigation_bar.public_timeline" diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 97651b5f4..def69d250 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1317,8 +1317,7 @@ .drawer__tab { display: block; flex: 1 1 auto; - padding: 15px; - padding-bottom: 13px; + padding: 15px 5px 13px; color: $ui-primary-color; text-decoration: none; text-align: center; -- cgit From e19eefe219c46ea9f763d0279029f03c5cf4554f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 11 Jul 2017 15:27:59 +0200 Subject: Redesign the landing page, mount public timeline on it (#4122) * Redesign the landing page, mount public timeline on it * Adjust the standalone mounted component to the lacking of router * Adjust auth layout pages to new design * Fix tests * Standalone public timeline polling every 5 seconds * Remove now obsolete translations * Add responsive design for new landing page * Address reviews * Add floating clouds behind frontpage form * Use access token from public page when available * Fix mentions and hashtags links, cursor on status content in standalone mode * Add footer link to source code * Fix errors on pages that don't embed the component, use classnames * Fix tests * Change anonymous autoPlayGif default to false * When gif autoplay is disabled, hover to play * Add option to hide the timeline preview * Slightly improve alt layout * Add elephant friend to new frontpage * Display "back to mastodon" in place of "login" when logged in on frontpage * Change polling time to 3s --- app/controllers/about_controller.rb | 13 +- app/controllers/admin/settings_controller.rb | 9 +- app/controllers/home_controller.rb | 16 +- .../fonts/montserrat/Montserrat-Medium.ttf | Bin 0 -> 192488 bytes app/javascript/images/cloud2.png | Bin 0 -> 4973 bytes app/javascript/images/cloud3.png | Bin 0 -> 5860 bytes app/javascript/images/cloud4.png | Bin 0 -> 5273 bytes app/javascript/images/elephant-fren.png | Bin 0 -> 40859 bytes app/javascript/images/logo.svg | 2 +- .../mastodon/components/dropdown_menu.js | 19 +- .../mastodon/components/media_gallery.js | 38 +- app/javascript/mastodon/components/permalink.js | 4 +- app/javascript/mastodon/components/status.js | 8 +- .../mastodon/components/status_action_bar.js | 11 +- .../mastodon/components/status_content.js | 17 +- app/javascript/mastodon/components/video_player.js | 22 +- .../mastodon/containers/timeline_container.js | 39 ++ .../features/standalone/public_timeline/index.js | 76 ++++ app/javascript/packs/public.js | 10 + app/javascript/styles/about.scss | 448 ++++++++++++++++++--- app/javascript/styles/basics.scss | 7 +- app/javascript/styles/boost.scss | 4 + app/javascript/styles/components.scss | 32 +- app/javascript/styles/containers.scss | 48 +-- app/javascript/styles/fonts/montserrat.scss | 8 + app/javascript/styles/forms.scss | 39 +- app/presenters/instance_presenter.rb | 1 + app/serializers/initial_state_serializer.rb | 35 +- app/views/about/_features.html.haml | 25 ++ app/views/about/_registration.html.haml | 20 +- app/views/about/show.html.haml | 120 +++--- app/views/admin/settings/edit.html.haml | 43 +- app/views/auth/registrations/new.html.haml | 6 +- app/views/layouts/auth.html.haml | 3 +- config/locales/ar.yml | 13 +- config/locales/bg.yml | 13 +- config/locales/ca.yml | 17 +- config/locales/de.yml | 13 - config/locales/en.yml | 43 +- config/locales/eo.yml | 23 +- config/locales/es.yml | 13 +- config/locales/fa.yml | 13 - config/locales/fi.yml | 13 +- config/locales/fr.yml | 15 +- config/locales/he.yml | 13 - config/locales/hr.yml | 13 +- config/locales/id.yml | 13 - config/locales/io.yml | 13 - config/locales/it.yml | 13 +- config/locales/ja.yml | 15 +- config/locales/ko.yml | 19 +- config/locales/nl.yml | 21 +- config/locales/no.yml | 13 - config/locales/oc.yml | 17 +- config/locales/pl.yml | 19 +- config/locales/pt-BR.yml | 13 - config/locales/pt.yml | 13 - config/locales/ru.yml | 13 - config/locales/th.yml | 13 - config/locales/tr.yml | 13 - config/locales/uk.yml | 13 - config/locales/zh-CN.yml | 13 - config/locales/zh-HK.yml | 13 - config/locales/zh-TW.yml | 13 - config/settings.yml | 1 + lib/tasks/mastodon.rake | 8 +- spec/requests/localization_spec.rb | 8 +- spec/views/about/show.html.haml_spec.rb | 9 +- 68 files changed, 956 insertions(+), 655 deletions(-) create mode 100644 app/javascript/fonts/montserrat/Montserrat-Medium.ttf create mode 100644 app/javascript/images/cloud2.png create mode 100644 app/javascript/images/cloud3.png create mode 100644 app/javascript/images/cloud4.png create mode 100644 app/javascript/images/elephant-fren.png create mode 100644 app/javascript/mastodon/containers/timeline_container.js create mode 100644 app/javascript/mastodon/features/standalone/public_timeline/index.js create mode 100644 app/views/about/_features.html.haml (limited to 'app/javascript/styles') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index c0addbecc..47690e81e 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,7 +4,10 @@ class AboutController < ApplicationController before_action :set_body_classes before_action :set_instance_presenter, only: [:show, :more, :terms] - def show; end + def show + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end def more; end @@ -15,6 +18,7 @@ class AboutController < ApplicationController def new_user User.new.tap(&:build_account) end + helper_method :new_user def set_instance_presenter @@ -24,4 +28,11 @@ class AboutController < ApplicationController def set_body_classes @body_classes = 'about-body' end + + def initial_state_params + { + settings: {}, + token: current_session&.token, + } + end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index f27a1f4d4..29b590d7a 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -11,8 +11,15 @@ module Admin site_terms open_registrations closed_registrations_message + open_deletion + timeline_preview + ).freeze + + BOOLEAN_SETTINGS = %w( + open_registrations + open_deletion + timeline_preview ).freeze - BOOLEAN_SETTINGS = %w(open_registrations).freeze def edit @settings = Setting.all_as_records diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 218da6906..8a8b9ec76 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -15,12 +15,16 @@ class HomeController < ApplicationController end def set_initial_state_json - state = InitialStatePresenter.new(settings: Web::Setting.find_by(user: current_user)&.data || {}, - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username)) - - serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer) + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = serializable_resource.to_json end + + def initial_state_params + { + settings: Web::Setting.find_by(user: current_user)&.data || {}, + current_account: current_account, + token: current_session.token, + admin: Account.find_local(Setting.site_contact_username), + } + end end diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf new file mode 100644 index 000000000..88d70b89c Binary files /dev/null and b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf differ diff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png new file mode 100644 index 000000000..f325ca6de Binary files /dev/null and b/app/javascript/images/cloud2.png differ diff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png new file mode 100644 index 000000000..ab194d0b8 Binary files /dev/null and b/app/javascript/images/cloud3.png differ diff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png new file mode 100644 index 000000000..98323f5a2 Binary files /dev/null and b/app/javascript/images/cloud4.png differ diff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png new file mode 100644 index 000000000..3b64edf08 Binary files /dev/null and b/app/javascript/images/elephant-fren.png differ diff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg index c233db842..16cb3a944 100644 --- a/app/javascript/images/logo.svg +++ b/app/javascript/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 12e1b44fa..98323b069 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent { size: PropTypes.number.isRequired, direction: PropTypes.string, ariaLabel: PropTypes.string, + disabled: PropTypes.bool, }; static defaultProps = { @@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent { } render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const { expanded } = this.state; + const { icon, items, size, direction, ariaLabel, disabled } = this.props; + const { expanded } = this.state; const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right'; + const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }; + const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`; + + if (disabled) { + return ( +
+ +
+ ); + } const dropdownItems = expanded && (