From 3567ac3d3efd9efcee770a0c4925fe5c86bb4d4f Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Wed, 12 Jul 2017 15:53:50 +0200 Subject: i18n: @e19eefe, @056b5ed + consistency improvement (pl) (#4171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n: @e19eefe (pl) Signed-off-by: Marcin Mikołajczak * i18n: @056b5ed (pl) Signed-off-by: Marcin Mikołajczak * i18n: Improve consistency (pl) Signed-off-by: Marcin Mikołajczak --- app/javascript/mastodon/locales/pl.json | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index ac63ec40f..c2288c4c0 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -3,10 +3,10 @@ "account.block_domain": "Blokuj wszystko z {domain}", "account.disclaimer": "Ten użytkownik pochodzi z innej instancji. Ta liczba może być większa.", "account.edit_profile": "Edytuj profil", - "account.follow": "Obserwuj", - "account.followers": "Obserwujący", - "account.follows": "Obserwacje", - "account.follows_you": "Obserwuje cię", + "account.follow": "Śledź", + "account.followers": "Śledzący", + "account.follows": "Śledzeni", + "account.follows_you": "Śledzi Cię", "account.media": "Media", "account.mention": "Wspomnij o @{name}", "account.mute": "Wycisz @{name}", @@ -15,7 +15,7 @@ "account.requested": "Oczekująca prośba", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Odblokuj domenę {domain}", - "account.unfollow": "Przestań obserwować", + "account.unfollow": "Przestań śledzić", "account.unmute": "Cofnij wyciszenie @{name}", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", @@ -27,7 +27,7 @@ "column.blocks": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.favourites": "Ulubione", - "column.follow_requests": "Prośby o obserwację", + "column.follow_requests": "Prośby o śledzenie", "column.home": "Strona główna", "column.mutes": "Wyciszeni użytkownicy", "column.notifications": "Powiadomienia", @@ -37,9 +37,9 @@ "column_header.unpin": "Cofnij przypięcie", "column_subheading.navigation": "Nawigacja", "column_subheading.settings": "Ustawienia", - "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto cię obserwuje, może wyświetlać twoje posty przeznaczone tylko dla obserwujących.", + "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje posty przeznaczone tylko dla śledzących.", "compose_form.lock_disclaimer.lock": "zablokowane", - "compose_form.placeholder": "Co ci chodzi po głowie?", + "compose_form.placeholder": "Co Ci chodzi po głowie?", "compose_form.privacy_disclaimer": "Twój post zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność postów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, post może być widoczny dla niewłaściwych osób.", "compose_form.publish": "Wyślij", "compose_form.publish_loud": "{publish}!", @@ -67,7 +67,7 @@ "emoji_button.travel": "Podróże i miejsca", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!", "empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", - "empty_column.home": "Nie obserwujesz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć ciekawych ludzi.", + "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.", "empty_column.home.public_timeline": "publiczna oś czasu", "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.", @@ -93,32 +93,32 @@ "navigation_bar.community_timeline": "Lokalna oś czasu", "navigation_bar.edit_profile": "Edytuj profil", "navigation_bar.favourites": "Ulubione", - "navigation_bar.follow_requests": "Prośby o obserwację", + "navigation_bar.follow_requests": "Prośby o śledzenie", "navigation_bar.info": "Szczegółowe informacje", "navigation_bar.logout": "Wyloguj", "navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.preferences": "Preferencje", "navigation_bar.public_timeline": "Oś czasu federacji", - "notification.favourite": "{name} dodał twój status do ulubionych", - "notification.follow": "{name} zaczął cię obserwować", + "notification.favourite": "{name} dodał Twój status do ulubionych", + "notification.follow": "{name} zaczął Cię śledzić", "notification.mention": "{name} wspomniał o tobie", - "notification.reblog": "{name} podbił twój status", + "notification.reblog": "{name} podbił Twój status", "notifications.clear": "Wyczyść powiadomienia", "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?", "notifications.column_settings.alert": "Powiadomienia na pulpicie", "notifications.column_settings.favourite": "Ulubione:", - "notifications.column_settings.follow": "Nowi obserwujący:", + "notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.mention": "Wspomniali:", "notifications.column_settings.reblog": "Podbili:", "notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.sound": "Odtwarzaj dźwięk", "onboarding.done": "Gotowe", "onboarding.next": "Dalej", - "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy obserwowanych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.", + "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.", "onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.", "onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.", "onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.", - "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc twój pełny adres to {handle}", + "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}", "onboarding.page_one.welcome": "Witamy w Mastodon!", "onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.", "onboarding.page_six.almost_done": "Prawie gotowe...", @@ -135,8 +135,8 @@ "privacy.change": "Dostosuj widoczność postów", "privacy.direct.long": "Widoczne tylko dla oznaczonych", "privacy.direct.short": "Bezpośrednio", - "privacy.private.long": "Widoczne tylko dla obserwujących", - "privacy.private.short": "Tylko obserwujący", + "privacy.private.long": "Widoczne tylko dla śledzących", + "privacy.private.short": "Tylko śledzący", "privacy.public.long": "Widoczne na publicznych osiach czasu", "privacy.public.short": "Publiczne", "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", -- cgit From 5abb3d815025675d1493da4f8c6e4dda4a6672e9 Mon Sep 17 00:00:00 2001 From: unarist Date: Thu, 13 Jul 2017 03:51:44 +0900 Subject: Rerender modal on property changes (#4175) Render function for BundleContainer must not be methods. React doesn't know dependency of the method, so they won't rerender on property updates. In this case, when you close modal and open another modal immediately, old modal will be open instead of new one. --- app/javascript/mastodon/features/ui/components/modal_root.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 085299038..4240871a7 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -54,12 +54,6 @@ 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 ; } @@ -95,7 +89,9 @@ export default class ModalRoot extends React.PureComponent {
- {this.renderModal} + + {(SpecificComponent) => } +
))} -- cgit From c29c20ab3cda3d4b752c67868925c1fe99d0ac71 Mon Sep 17 00:00:00 2001 From: unarist Date: Thu, 13 Jul 2017 21:49:57 +0900 Subject: Add background color for spoiler input, like toot textarea (#4181) --- app/javascript/styles/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 8d0350eb6..45dd9f914 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1748,6 +1748,7 @@ width: 100%; margin: 0; color: $ui-base-color; + background: $simple-background-color; padding: 10px; font-family: inherit; font-size: 14px; @@ -1770,7 +1771,6 @@ .autosuggest-textarea__textarea { min-height: 100px; - background: $simple-background-color; border-radius: 4px 4px 0 0; padding-bottom: 0; padding-right: 10px + 22px; -- cgit From 0c7c188c459117770ac1f74f70a9e65ed2be606f Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Thu, 13 Jul 2017 22:15:32 +0200 Subject: Web Push Notifications (#3243) * feat: Register push subscription * feat: Notify when mentioned * feat: Boost, favourite, reply, follow, follow request * feat: Notification interaction * feat: Handle change of public key * feat: Unsubscribe if things go wrong * feat: Do not send normal notifications if push is enabled * feat: Focus client if open * refactor: Move push logic to WebPushSubscription * feat: Better title and body * feat: Localize messages * chore: Fix lint errors * feat: Settings * refactor: Lazy load * fix: Check if push settings exist * feat: Device-based preferences * refactor: Simplify logic * refactor: Pull request feedback * refactor: Pull request feedback * refactor: Create /api/web/push_subscriptions endpoint * feat: Spec PushSubscriptionController * refactor: WebPushSubscription => Web::PushSubscription * feat: Spec Web::PushSubscription * feat: Display first media attachment * feat: Support direction * fix: Stuff broken while rebasing * refactor: Integration with session activations * refactor: Cleanup * refactor: Simplify implementation * feat: Set VAPID keys via environment * chore: Comments * fix: Crash when no alerts * fix: Set VAPID keys in testing environment * fix: Follow link * feat: Notification actions * fix: Delete previous subscription * chore: Temporary logs * refactor: Move migration to a later date * fix: Fetch the correct session activation and misc bugs * refactor: Move migration to a later date * fix: Remove follow request (no notifications) * feat: Send administrator contact to push service * feat: Set time-to-live * fix: Do not show sensitive images * fix: Reducer crash in error handling * feat: Add badge * chore: Fix lint error * fix: Checkbox label overlap * fix: Check for payload support * fix: Rename action "type" (crash in latest Chrome) * feat: Action to expand notification * fix: Lint errors * fix: Unescape notification body * fix: Do not allow boosting if the status is hidden * feat: Add VAPID keys to the production sample environment * fix: Strip HTML tags from status * refactor: Better error messages * refactor: Handle browser not implementing the VAPID protocol (Samsung Internet) * fix: Error when target_status is nil * fix: Handle lack of image * fix: Delete reference to invalid subscriptions * feat: Better error handling * fix: Unescape HTML characters after tags are striped * refactor: Simpify code * fix: Modify to work with #4091 * Sort strings alphabetically * i18n: Updated Polish translation it annoys me that it's not fully localized :P * refactor: Use current_session in PushSubscriptionController * fix: Rebase mistake * fix: Set cacheName to mastodon * refactor: Pull request feedback * refactor: Remove logging statements * chore(yarn): Fix conflicts with master * chore(yarn): Copy latest from master * chore(yarn): Readd offline-plugin * refactor: Use save! and update! * refactor: Send notifications async * fix: Allow retry when push fails * fix: Save track for failed pushes * fix: Minify sw.js * fix: Remove account_id from fabricator --- .env.production.sample | 11 ++ .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 6 + .../api/web/push_subscriptions_controller.rb | 39 +++++ app/controllers/home_controller.rb | 1 + .../mastodon/actions/push_notifications.js | 52 ++++++ .../notifications/components/column_settings.js | 23 ++- .../notifications/components/setting_toggle.js | 4 +- .../containers/column_settings_container.js | 9 +- app/javascript/mastodon/main.js | 8 + app/javascript/mastodon/reducers/index.js | 2 + .../mastodon/reducers/push_notifications.js | 51 ++++++ app/javascript/mastodon/service_worker/entry.js | 1 + .../service_worker/web_push_notifications.js | 86 ++++++++++ app/javascript/mastodon/web_push_subscription.js | 109 ++++++++++++ app/javascript/styles/components.scss | 8 +- app/javascript/styles/rtl.scss | 4 + app/models/session_activation.rb | 12 ++ app/models/user.rb | 4 + app/models/web/push_subscription.rb | 190 +++++++++++++++++++++ app/presenters/initial_state_presenter.rb | 2 +- app/serializers/initial_state_serializer.rb | 2 +- app/services/notify_service.rb | 5 + app/views/home/index.html.haml | 1 + app/workers/web_push_notification_worker.rb | 27 +++ config/environments/development.rb | 5 + config/environments/test.rb | 5 + config/initializers/vapid.rb | 17 ++ config/locales/en.yml | 15 ++ config/locales/pl.yml | 15 ++ config/routes.rb | 5 + config/webpack/production.js | 14 ++ ...20170713175513_create_web_push_subscriptions.rb | 12 ++ ...web_push_subscription_to_session_activations.rb | 5 + db/schema.rb | 12 +- package.json | 1 + public/badge.png | Bin 0 -> 31156 bytes .../api/web/push_subscriptions_controller_spec.rb | 81 +++++++++ .../web_push_subscription_fabricator.rb | 5 + spec/models/web/push_subscription_spec.rb | 28 +++ yarn.lock | 25 ++- 42 files changed, 890 insertions(+), 14 deletions(-) create mode 100644 app/controllers/api/web/push_subscriptions_controller.rb create mode 100644 app/javascript/mastodon/actions/push_notifications.js create mode 100644 app/javascript/mastodon/reducers/push_notifications.js create mode 100644 app/javascript/mastodon/service_worker/entry.js create mode 100644 app/javascript/mastodon/service_worker/web_push_notifications.js create mode 100644 app/javascript/mastodon/web_push_subscription.js create mode 100644 app/models/web/push_subscription.rb create mode 100644 app/workers/web_push_notification_worker.rb create mode 100644 config/initializers/vapid.rb create mode 100644 db/migrate/20170713175513_create_web_push_subscriptions.rb create mode 100644 db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb create mode 100644 public/badge.png create mode 100644 spec/controllers/api/web/push_subscriptions_controller_spec.rb create mode 100644 spec/fabricators/web_push_subscription_fabricator.rb create mode 100644 spec/models/web/push_subscription_spec.rb (limited to 'app/javascript') diff --git a/.env.production.sample b/.env.production.sample index 394cdedfe..faefa2482 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -31,6 +31,17 @@ PAPERCLIP_SECRET= SECRET_KEY_BASE= OTP_SECRET= +# VAPID keys (used for push notifications +# You can generate the keys using the following command (first is the private key, second is the public one) +# You should only generate this once per instance. If you later decide to change it, all push subscription will +# be invalidated, requiring the users to access the website again to resubscribe. +# +# ruby -e "require 'webpush'; vapid_key = Webpush.generate_key; puts vapid_key.private_key; puts vapid_key.public_key;" +# +# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html +VAPID_PRIVATE_KEY= +VAPID_PUBLIC_KEY= + # Registrations # Single user mode will disable registrations and redirect frontpage to the first profile # SINGLE_USER_MODE=true diff --git a/.gitignore b/.gitignore index 38ebc934f..868a84368 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ public/system public/assets public/packs public/packs-test +public/sw.js .env .env.production node_modules/ diff --git a/Gemfile b/Gemfile index b52685cba..988b4d6b9 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'statsd-instrument', '~> 2.1' gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2017' gem 'webpacker', '~> 2.0' +gem 'webpush' group :development, :test do gem 'fabrication', '~> 2.16' diff --git a/Gemfile.lock b/Gemfile.lock index de0d6a107..5599e1db1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -181,6 +181,7 @@ GEM hashdiff (0.3.4) highline (1.7.8) hiredis (0.6.1) + hkdf (0.3.0) htmlentities (4.3.4) http (2.2.2) addressable (~> 2.3) @@ -209,6 +210,7 @@ GEM jmespath (1.3.1) json (2.1.0) jsonapi-renderer (0.1.2) + jwt (1.5.6) kaminari (1.0.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.0.1) @@ -475,6 +477,9 @@ GEM activesupport (>= 4.2) multi_json (~> 1.2) railties (>= 4.2) + webpush (0.3.2) + hkdf (~> 0.2) + jwt websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -573,6 +578,7 @@ DEPENDENCIES uglifier (~> 3.2) webmock (~> 3.0) webpacker (~> 2.0) + webpush RUBY VERSION ruby 2.4.1p111 diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb new file mode 100644 index 000000000..8425db7b4 --- /dev/null +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Api::Web::PushSubscriptionsController < Api::BaseController + respond_to :json + + before_action :require_user! + + def create + params.require(:data).require(:endpoint) + params.require(:data).require(:keys).require([:auth, :p256dh]) + + active_session = current_session + + unless active_session.web_push_subscription.nil? + active_session.web_push_subscription.destroy! + active_session.update!(web_push_subscription: nil) + end + + web_subscription = ::Web::PushSubscription.create!( + endpoint: params[:data][:endpoint], + key_p256dh: params[:data][:keys][:p256dh], + key_auth: params[:data][:keys][:auth] + ) + + active_session.update!(web_push_subscription: web_subscription) + + render json: web_subscription.as_payload + end + + def update + params.require([:id, :data]) + + web_subscription = ::Web::PushSubscription.find(params[:id]) + + web_subscription.update!(data: params[:data]) + + render json: web_subscription.as_payload + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 8a8b9ec76..1585bc810 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -22,6 +22,7 @@ class HomeController < ApplicationController def initial_state_params { settings: Web::Setting.find_by(user: current_user)&.data || {}, + push_subscription: current_account.user.web_push_subscription(current_session), current_account: current_account, token: current_session.token, admin: Account.find_local(Setting.site_contact_username), diff --git a/app/javascript/mastodon/actions/push_notifications.js b/app/javascript/mastodon/actions/push_notifications.js new file mode 100644 index 000000000..55661d2b0 --- /dev/null +++ b/app/javascript/mastodon/actions/push_notifications.js @@ -0,0 +1,52 @@ +import axios from 'axios'; + +export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; +export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; +export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; + +export function setBrowserSupport (value) { + return { + type: SET_BROWSER_SUPPORT, + value, + }; +} + +export function setSubscription (subscription) { + return { + type: SET_SUBSCRIPTION, + subscription, + }; +} + +export function clearSubscription () { + return { + type: CLEAR_SUBSCRIPTION, + }; +} + +export function changeAlerts(key, value) { + return dispatch => { + dispatch({ + type: ALERTS_CHANGE, + key, + value, + }); + + dispatch(saveSettings()); + }; +} + +export function saveSettings() { + return (_, getState) => { + const state = getState().get('push_notifications'); + const subscription = state.get('subscription'); + const alerts = state.get('alerts'); + + axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { + data: { + alerts, + }, + }); + }; +} diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 260594894..31cac5bc7 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -9,18 +9,27 @@ export default class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, + pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, }; + onPushChange = (key, checked) => { + this.props.onChange(['push', ...key], checked); + } + render () { - const { settings, onChange, onClear } = this.props; + const { settings, pushSettings, onChange, onClear } = this.props; const alertStr = ; const showStr = ; const soundStr = ; + const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); + const pushStr = showPushSettings && ; + const pushMeta = showPushSettings && ; + return (
@@ -30,7 +39,8 @@ export default class ColumnSettings extends React.PureComponent {
- + + {showPushSettings && }
@@ -38,7 +48,8 @@ export default class ColumnSettings extends React.PureComponent {
- + + {showPushSettings && }
@@ -46,7 +57,8 @@ export default class ColumnSettings extends React.PureComponent {
- + + {showPushSettings && }
@@ -54,7 +66,8 @@ export default class ColumnSettings extends React.PureComponent {
- + + {showPushSettings && }
diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 510820358..be1ff91d6 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -10,6 +10,7 @@ export default class SettingToggle extends React.PureComponent { settings: ImmutablePropTypes.map.isRequired, settingKey: PropTypes.array.isRequired, label: PropTypes.node.isRequired, + meta: PropTypes.node, onChange: PropTypes.func.isRequired, } @@ -18,13 +19,14 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingKey, label } = this.props; + const { prefix, settings, settingKey, label, meta } = this.props; const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-'); return (
+ {meta && {meta}}
); } diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index b139d4615..d4ead7881 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -3,6 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ColumnSettings from '../components/column_settings'; import { changeSetting, saveSettings } from '../../../actions/settings'; import { clearNotifications } from '../../../actions/notifications'; +import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications'; import { openModal } from '../../../actions/modal'; const messages = defineMessages({ @@ -12,16 +13,22 @@ const messages = defineMessages({ const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), + pushSettings: state.get('push_notifications'), }); const mapDispatchToProps = (dispatch, { intl }) => ({ onChange (key, checked) { - dispatch(changeSetting(['notifications', ...key], checked)); + if (key[0] === 'push') { + dispatch(changePushNotifications(key.slice(1), checked)); + } else { + dispatch(changeSetting(['notifications', ...key], checked)); + } }, onSave () { dispatch(saveSettings()); + dispatch(savePushNotificationSettings()); }, onClear () { diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index d7ffa8ea6..d2c9d1c94 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -29,6 +29,14 @@ function main() { const props = JSON.parse(mountNode.getAttribute('data-props')); ReactDOM.render(, mountNode); + if (process.env.NODE_ENV === 'production') { + // avoid offline in dev mode because it's harder to debug + const OfflinePluginRuntime = require('offline-plugin/runtime'); + const WebPushSubscription = require('./web_push_subscription'); + + OfflinePluginRuntime.install(); + WebPushSubscription.register(); + } perf.stop('main()'); }); } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 919345f16..3aaf259c2 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -10,6 +10,7 @@ import accounts_counters from './accounts_counters'; import statuses from './statuses'; import relationships from './relationships'; import settings from './settings'; +import push_notifications from './push_notifications'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; @@ -32,6 +33,7 @@ const reducers = { statuses, relationships, settings, + push_notifications, cards, reports, contexts, diff --git a/app/javascript/mastodon/reducers/push_notifications.js b/app/javascript/mastodon/reducers/push_notifications.js new file mode 100644 index 000000000..31a40d246 --- /dev/null +++ b/app/javascript/mastodon/reducers/push_notifications.js @@ -0,0 +1,51 @@ +import { STORE_HYDRATE } from '../actions/store'; +import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from '../actions/push_notifications'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + subscription: null, + alerts: new Immutable.Map({ + follow: false, + favourite: false, + reblog: false, + mention: false, + }), + isSubscribed: false, + browserSupport: false, +}); + +export default function push_subscriptions(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: { + const push_subscription = action.state.get('push_subscription'); + + if (push_subscription) { + return state + .set('subscription', new Immutable.Map({ + id: push_subscription.get('id'), + endpoint: push_subscription.get('endpoint'), + })) + .set('alerts', push_subscription.get('alerts') || initialState.get('alerts')) + .set('isSubscribed', true); + } + + return state; + } + case SET_SUBSCRIPTION: + return state + .set('subscription', new Immutable.Map({ + id: action.subscription.id, + endpoint: action.subscription.endpoint, + })) + .set('alerts', new Immutable.Map(action.subscription.alerts)) + .set('isSubscribed', true); + case SET_BROWSER_SUPPORT: + return state.set('browserSupport', action.value); + case CLEAR_SUBSCRIPTION: + return initialState; + case ALERTS_CHANGE: + return state.setIn(action.key, action.value); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js new file mode 100644 index 000000000..364b67066 --- /dev/null +++ b/app/javascript/mastodon/service_worker/entry.js @@ -0,0 +1 @@ +import './web_push_notifications'; diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js new file mode 100644 index 000000000..1708aa9f7 --- /dev/null +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -0,0 +1,86 @@ +const handlePush = (event) => { + const options = event.data.json(); + + options.body = options.data.nsfw || options.data.content; + options.image = options.image || undefined; // Null results in a network request (404) + options.timestamp = options.timestamp && new Date(options.timestamp); + + const expandAction = options.data.actions.find(action => action.todo === 'expand'); + + if (expandAction) { + options.actions = [expandAction]; + options.hiddenActions = options.data.actions.filter(action => action !== expandAction); + + options.data.hiddenImage = options.image; + options.image = undefined; + } else { + options.actions = options.data.actions; + } + + event.waitUntil(self.registration.showNotification(options.title, options)); +}; + +const cloneNotification = (notification) => { + const clone = { }; + + for(var k in notification) { + clone[k] = notification[k]; + } + + return clone; +}; + +const expandNotification = (notification) => { + const nextNotification = cloneNotification(notification); + + nextNotification.body = notification.data.content; + nextNotification.image = notification.data.hiddenImage; + nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); + + return self.registration.showNotification(nextNotification.title, nextNotification); +}; + +const makeRequest = (notification, action) => + fetch(action.action, { + headers: { + 'Authorization': `Bearer ${notification.data.access_token}`, + 'Content-Type': 'application/json', + }, + method: action.method, + credentials: 'include', + }); + +const removeActionFromNotification = (notification, action) => { + const actions = notification.actions.filter(act => act.action !== action.action); + + const nextNotification = cloneNotification(notification); + + nextNotification.actions = actions; + + return self.registration.showNotification(nextNotification.title, nextNotification); +}; + +const handleNotificationClick = (event) => { + const reactToNotificationClick = new Promise((resolve, reject) => { + if (event.action) { + const action = event.notification.data.actions.find(({ action }) => action === event.action); + + if (action.todo === 'expand') { + resolve(expandNotification(event.notification)); + } else if (action.todo === 'request') { + resolve(makeRequest(event.notification, action) + .then(() => removeActionFromNotification(event.notification, action))); + } else { + reject(`Unknown action: ${action.todo}`); + } + } else { + event.notification.close(); + resolve(self.clients.openWindow(event.notification.data.url)); + } + }); + + event.waitUntil(reactToNotificationClick); +}; + +self.addEventListener('push', handlePush); +self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js new file mode 100644 index 000000000..391d3bcec --- /dev/null +++ b/app/javascript/mastodon/web_push_subscription.js @@ -0,0 +1,109 @@ +import axios from 'axios'; +import { store } from './containers/mastodon'; +import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications'; + +// Taken from https://www.npmjs.com/package/web-push +const urlBase64ToUint8Array = (base64String) => { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +}; + +const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); + +const getRegistration = () => navigator.serviceWorker.ready; + +const getPushSubscription = (registration) => + registration.pushManager.getSubscription() + .then(subscription => ({ registration, subscription })); + +const subscribe = (registration) => + registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), + }); + +const unsubscribe = ({ registration, subscription }) => + subscription ? subscription.unsubscribe().then(() => registration) : registration; + +const sendSubscriptionToBackend = (subscription) => + axios.post('/api/web/push_subscriptions', { + data: subscription, + }).then(response => response.data); + +// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload +const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); + +export function register () { + store.dispatch(setBrowserSupport(supportsPushNotifications)); + + if (supportsPushNotifications) { + if (!getApplicationServerKey()) { + // eslint-disable-next-line no-console + console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); + return; + } + + getRegistration() + .then(getPushSubscription) + .then(({ registration, subscription }) => { + if (subscription !== null) { + // We have a subscription, check if it is still valid + const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); + const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); + const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']); + + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid + if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { + return subscription; + } else { + // Something went wrong, try to subscribe again + return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend); + } + } + + // No subscription, try to subscribe + return subscribe(registration).then(sendSubscriptionToBackend); + }) + .then(subscription => { + // If we got a PushSubscription (and not a subscription object from the backend) + // it means that the backend subscription is valid (and was set during hydration) + if (!(subscription instanceof PushSubscription)) { + store.dispatch(setSubscription(subscription)); + } + }) + .catch(error => { + if (error.code === 20 && error.name === 'AbortError') { + // eslint-disable-next-line no-console + console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); + } else if (error.code === 5 && error.name === 'InvalidCharacterError') { + // eslint-disable-next-line no-console + console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); + } + + // Clear alerts and hide UI settings + store.dispatch(clearSubscription()); + + try { + getRegistration() + .then(getPushSubscription) + .then(unsubscribe); + } catch (e) { + + } + }); + } else { + // eslint-disable-next-line no-console + console.warn('Your browser does not support Web Push Notifications.'); + } +} diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 45dd9f914..02602afa4 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2352,7 +2352,8 @@ button.icon-button.active i.fa-retweet { line-height: 24px; } -.setting-toggle__label { +.setting-toggle__label, +.setting-meta__label { color: $ui-primary-color; display: inline-block; margin-bottom: 14px; @@ -2360,6 +2361,11 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } +.setting-meta__label { + color: $ui-primary-color; + float: right; +} + .empty-column-indicator, .error-column { color: lighten($ui-base-color, 20%); diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss index a91d0d72a..4966fbc21 100644 --- a/app/javascript/styles/rtl.scss +++ b/app/javascript/styles/rtl.scss @@ -45,6 +45,10 @@ body.rtl { margin-right: 8px; } + .setting-meta__label { + float: left; + } + .status__avatar { left: auto; right: 10px; diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 887e3e3bd..7eb16af8f 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -3,6 +3,17 @@ # # Table name: session_activations # +# id :integer not null, primary key +# user_id :integer not null +# session_id :string not null +# created_at :datetime not null +# updated_at :datetime not null +# user_agent :string default(""), not null +# ip :inet +# access_token_id :integer +# web_push_subscription_id :integer +# + # id :integer not null, primary key # user_id :integer not null # session_id :string not null @@ -15,6 +26,7 @@ class SessionActivation < ApplicationRecord belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy + belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy delegate :token, to: :access_token, diff --git a/app/models/user.rb b/app/models/user.rb index 86e578225..a63b1da7f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -113,6 +113,10 @@ class User < ApplicationRecord session_activations.active? id end + def web_push_subscription(session) + session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload + end + protected def send_devise_notification(notification, *args) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb new file mode 100644 index 000000000..4440706a6 --- /dev/null +++ b/app/models/web/push_subscription.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: web_push_subscriptions +# +# id :integer not null, primary key +# endpoint :string not null +# key_p256dh :string not null +# key_auth :string not null +# data :json +# created_at :datetime not null +# updated_at :datetime not null +# + +class Web::PushSubscription < ApplicationRecord + include RoutingHelper + include StreamEntriesHelper + include ActionView::Helpers::TranslationHelper + include ActionView::Helpers::SanitizeHelper + + has_one :session_activation + + before_create :send_welcome_notification + + def push(notification) + return unless pushable? notification + + name = display_name notification.from_account + title = title_str(name, notification) + body = body_str notification + dir = dir_str body + url = url_str notification + image = image_str notification + actions = actions_arr notification + + access_token = actions.empty? ? nil : find_or_create_access_token(notification).token + nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text + + # TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge + # TODO: Queue the requests - Webpush::TooManyRequests + Webpush.payload_send( + message: JSON.generate( + title: title, + dir: dir, + image: image, + badge: full_asset_url('badge.png'), + tag: notification.id, + timestamp: notification.created_at, + icon: notification.from_account.avatar_static_url, + data: { + content: decoder.decode(strip_tags(body)), + nsfw: nsfw.nil? ? nil : decoder.decode(strip_tags(nsfw)), + url: url, + actions: actions, + access_token: access_token, + } + ), + endpoint: endpoint, + p256dh: key_p256dh, + auth: key_auth, + vapid: { + # subject: "mailto:#{Setting.site_contact_email}", + private_key: Rails.configuration.x.vapid_private_key, + public_key: Rails.configuration.x.vapid_public_key, + }, + ttl: 40 * 60 * 60 # 48 hours + ) + end + + def as_payload + payload = { + id: id, + endpoint: endpoint, + } + + payload[:alerts] = data['alerts'] if data && data.key?('alerts') + + payload + end + + private + + def title_str(name, notification) + case notification.type + when :mention then translate('push_notifications.mention.title', name: name) + when :follow then translate('push_notifications.follow.title', name: name) + when :favourite then translate('push_notifications.favourite.title', name: name) + when :reblog then translate('push_notifications.reblog.title', name: name) + end + end + + def body_str(notification) + case notification.type + when :mention then notification.target_status.text + when :follow then notification.from_account.note + when :favourite then notification.target_status.text + when :reblog then notification.target_status.text + end + end + + def url_str(notification) + case notification.type + when :mention then web_url("statuses/#{notification.target_status.id}") + when :follow then web_url("accounts/#{notification.from_account.id}") + when :favourite then web_url("statuses/#{notification.target_status.id}") + when :reblog then web_url("statuses/#{notification.target_status.id}") + end + end + + def actions_arr(notification) + actions = + case notification.type + when :mention then [ + { + title: translate('push_notifications.mention.action_favourite'), + icon: full_asset_url('emoji/2764.png'), + todo: 'request', + method: 'POST', + action: "/api/v1/statuses/#{notification.target_status.id}/favourite", + }, + ] + else [] + end + + should_hide = notification.type.equal?(:mention) && !notification.target_status.nil? && (notification.target_status.sensitive || !notification.target_status.spoiler_text.empty?) + can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden? + + if should_hide + actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand') + end + + if can_boost + actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" } + end + + actions + end + + def image_str(notification) + return nil if notification.target_status.nil? || notification.target_status.media_attachments.empty? + + full_asset_url(notification.target_status.media_attachments.first.file.url(:small)) + end + + def dir_str(body) + rtl?(body) ? 'rtl' : 'ltr' + end + + def pushable?(notification) + data && data.key?('alerts') && data['alerts'][notification.type.to_s] + end + + def send_welcome_notification + Webpush.payload_send( + message: JSON.generate( + title: translate('push_notifications.subscribed.title'), + icon: full_asset_url('android-chrome-192x192.png'), + badge: full_asset_url('badge.png'), + data: { + content: translate('push_notifications.subscribed.body'), + actions: [], + url: web_url('notifications'), + } + ), + endpoint: endpoint, + p256dh: key_p256dh, + auth: key_auth, + vapid: { + # subject: "mailto:#{Setting.site_contact_email}", + private_key: Rails.configuration.x.vapid_private_key, + public_key: Rails.configuration.x.vapid_public_key, + }, + ttl: 5 * 60 # 5 minutes + ) + end + + def find_or_create_access_token(notification) + Doorkeeper::AccessToken.find_or_create_for( + Doorkeeper::Application.find_by(superapp: true), + notification.account.user.id, + Doorkeeper::OAuth::Scopes.from_string('read write follow'), + Doorkeeper.configuration.access_token_expires_in, + Doorkeeper.configuration.refresh_token_enabled? + ) + end + + def decoder + @decoder ||= HTMLEntities.new + end +end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb index 75fef28a8..9507aad4a 100644 --- a/app/presenters/initial_state_presenter.rb +++ b/app/presenters/initial_state_presenter.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class InitialStatePresenter < ActiveModelSerializers::Model - attributes :settings, :token, :current_account, :admin + attributes :settings, :push_subscription, :token, :current_account, :admin end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 6751c9411..704d29a57 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -2,7 +2,7 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, - :media_attachments, :settings + :media_attachments, :settings, :push_subscription def meta store = { diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 407d385ea..0ab61b634 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -61,6 +61,11 @@ class NotifyService < BaseService @notification.save! return unless @notification.browserable? Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) + send_push_notifications + end + + def send_push_notifications + WebPushNotificationWorker.perform_async(@recipient.id, @notification.id) end def send_email diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 71dcb54c6..13ca9ea79 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,4 +1,5 @@ - content_for :header_tags do + %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb new file mode 100644 index 000000000..0568a3e02 --- /dev/null +++ b/app/workers/web_push_notification_worker.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class WebPushNotificationWorker + include Sidekiq::Worker + + sidekiq_options backtrace: true + + def perform(recipient_id, notification_id) + recipient = Account.find(recipient_id) + notification = Notification.find(notification_id) + + sessions_with_subscriptions = recipient.user.session_activations.reject { |session| session.web_push_subscription.nil? } + + sessions_with_subscriptions.each do |session| + begin + session.web_push_subscription.push(notification) + rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription + # Subscription expiration is not currently implemented in any browser + session.web_push_subscription.destroy! + session.web_push_subscription = nil + session.save! + rescue Webpush::PayloadTooLarge => e + Rails.logger.error(e) + end + end + end +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 406fa970b..4c60965c8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -31,6 +31,11 @@ Rails.application.configure do config.logger = ActiveSupport::TaggedLogging.new(logger) end + # Generate random VAPID keys + vapid_key = Webpush.generate_key + config.x.vapid_private_key = vapid_key.private_key + config.x.vapid_public_key = vapid_key.public_key + # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb index bde69eba1..e68cb156d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -40,6 +40,11 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Generate random VAPID keys + vapid_key = Webpush.generate_key + config.x.vapid_private_key = vapid_key.private_key + config.x.vapid_public_key = vapid_key.public_key + # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end diff --git a/config/initializers/vapid.rb b/config/initializers/vapid.rb new file mode 100644 index 000000000..74e07377c --- /dev/null +++ b/config/initializers/vapid.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +Rails.application.configure do + + # You can generate the keys using the following command (first is the private key, second is the public one) + # You should only generate this once per instance. If you later decide to change it, all push subscription will + # be invalidated, requiring the users to access the website again to resubscribe. + # + # ruby -e "require 'webpush'; vapid_key = Webpush.generate_key; puts vapid_key.private_key; puts vapid_key.public_key;" + # + # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html + + if Rails.env.production? + config.x.vapid_private_key = ENV['VAPID_PRIVATE_KEY'] + config.x.vapid_public_key = ENV['VAPID_PUBLIC_KEY'] + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index c9b5d9ab8..79efddfad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -335,6 +335,21 @@ en: next: Next prev: Prev truncate: "…" + push_notifications: + favourite: + title: "%{name} favourited your status" + follow: + title: "%{name} is now following you" + mention: + action_boost: 'Boost' + action_expand: 'Show more' + action_favourite: 'Favourite' + title: "%{name} mentioned you" + reblog: + title: "%{name} boosted your status" + subscribed: + body: "You can now receive push notifications." + title: "Subscription registered!" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account diff --git a/config/locales/pl.yml b/config/locales/pl.yml index dc5aa716b..f9d69745f 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -339,6 +339,21 @@ pl: next: Następna prev: Poprzednia truncate: "…" + push_notifications: + favourite: + title: "%{name} dodał Twój status do ulubionych" + follow: + title: "%{name} zaczął Cię śledzić" + mention: + action_boost: 'Podbij' + action_expand: 'Pokaż więcej' + action_favourite: 'Dodaj do ulubionych' + title: "%{name} wspomniał o Tobie" + reblog: + title: "%{name} podbił Twój status" + subscribed: + body: "Otrzymujesz teraz powiadomienia push." + title: "Zarejestrowano subskrypcję!" remote_follow: acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny diff --git a/config/routes.rb b/config/routes.rb index 963fedcb4..9171d02d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -206,6 +206,11 @@ Rails.application.routes.draw do namespace :web do resource :settings, only: [:update] + resources :push_subscriptions, only: [:create] do + member do + put :update + end + end end end diff --git a/config/webpack/production.js b/config/webpack/production.js index 303fca81b..4592db89e 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -5,6 +5,9 @@ const merge = require('webpack-merge'); const CompressionPlugin = require('compression-webpack-plugin'); const sharedConfig = require('./shared.js'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const OfflinePlugin = require('offline-plugin'); +const { publicPath } = require('./configuration.js'); +const path = require('path'); module.exports = merge(sharedConfig, { output: { filename: '[name]-[chunkhash].js' }, @@ -39,5 +42,16 @@ module.exports = merge(sharedConfig, { openAnalyzer: false, logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout }), + new OfflinePlugin({ + publicPath: publicPath, // sw.js must be served from the root to avoid scope issues + caches: { }, // do not cache things, we only use it for push notifications for now + ServiceWorker: { + entry: path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'), + cacheName: 'mastodon', + output: '../sw.js', + publicPath: '/sw.js', + minify: true, + }, + }), ], }); diff --git a/db/migrate/20170713175513_create_web_push_subscriptions.rb b/db/migrate/20170713175513_create_web_push_subscriptions.rb new file mode 100644 index 000000000..4e5c2ba00 --- /dev/null +++ b/db/migrate/20170713175513_create_web_push_subscriptions.rb @@ -0,0 +1,12 @@ +class CreateWebPushSubscriptions < ActiveRecord::Migration[5.1] + def change + create_table :web_push_subscriptions do |t| + t.string :endpoint, null: false + t.string :key_p256dh, null: false + t.string :key_auth, null: false + t.json :data + + t.timestamps + end + end +end diff --git a/db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb b/db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb new file mode 100644 index 000000000..d69cdfa50 --- /dev/null +++ b/db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb @@ -0,0 +1,5 @@ +class AddWebPushSubscriptionToSessionActivations < ActiveRecord::Migration[5.1] + def change + add_column :session_activations, :web_push_subscription_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index d6e572703..b2c59a0f6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170713112503) do +ActiveRecord::Schema.define(version: 20170713190709) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -258,6 +258,7 @@ ActiveRecord::Schema.define(version: 20170713112503) do t.string "user_agent", default: "", null: false t.inet "ip" t.integer "access_token_id" + t.integer "web_push_subscription_id" t.index ["session_id"], name: "index_session_activations_on_session_id", unique: true t.index ["user_id"], name: "index_session_activations_on_user_id" end @@ -371,6 +372,15 @@ ActiveRecord::Schema.define(version: 20170713112503) do t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + create_table "web_push_subscriptions", force: :cascade do |t| + t.string "endpoint", null: false + t.string "key_p256dh", null: false + t.string "key_auth", null: false + t.json "data" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "web_settings", id: :serial, force: :cascade do |t| t.integer "user_id" t.json "data" diff --git a/package.json b/package.json index 004c4d1f5..1aaa243c8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "node-sass": "^4.5.2", "npmlog": "^4.1.2", "object-assign": "^4.1.1", + "offline-plugin": "^4.8.3", "path-complete-extname": "^0.1.0", "pg": "^6.4.0", "postcss-loader": "^2.0.6", diff --git a/public/badge.png b/public/badge.png new file mode 100644 index 000000000..fc1f42dca Binary files /dev/null and b/public/badge.png differ diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb new file mode 100644 index 000000000..871176a07 --- /dev/null +++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::Web::PushSubscriptionsController do + render_views + + let(:user) { Fabricate(:user) } + + let(:create_payload) do + { + data: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + } + } + end + + let(:alerts_payload) do + { + data: { + alerts: { + follow: true, + favourite: false, + reblog: true, + mention: false, + } + } + } + end + + describe 'POST #create' do + it 'saves push subscriptions' do + sign_in(user) + + stub_request(:post, create_payload[:data][:endpoint]).to_return(status: 200) + + post :create, format: :json, params: create_payload + + user.reload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:data][:endpoint]) + + expect(push_subscription['endpoint']).to eq(create_payload[:data][:endpoint]) + expect(push_subscription['key_p256dh']).to eq(create_payload[:data][:keys][:p256dh]) + expect(push_subscription['key_auth']).to eq(create_payload[:data][:keys][:auth]) + end + + it 'sends welcome notification' do + sign_in(user) + + stub_request(:post, create_payload[:data][:endpoint]).to_return(status: 200) + + post :create, format: :json, params: create_payload + end + end + + describe 'PUT #update' do + it 'changes alert settings' do + sign_in(user) + + stub_request(:post, create_payload[:data][:endpoint]).to_return(status: 200) + + post :create, format: :json, params: create_payload + + alerts_payload[:id] = Web::PushSubscription.find_by(endpoint: create_payload[:data][:endpoint]).id + + put :update, format: :json, params: alerts_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:data][:endpoint]) + + expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) + expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) + expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) + expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + end + end +end diff --git a/spec/fabricators/web_push_subscription_fabricator.rb b/spec/fabricators/web_push_subscription_fabricator.rb new file mode 100644 index 000000000..72d11b77c --- /dev/null +++ b/spec/fabricators/web_push_subscription_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:web_push_subscription) do + endpoint Faker::Internet.url + key_p256dh Faker::Internet.password + key_auth Faker::Internet.password +end diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb new file mode 100644 index 000000000..574da55ac --- /dev/null +++ b/spec/models/web/push_subscription_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe Web::PushSubscription, type: :model do + let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } } + let(:payload_no_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd').as_payload } + let(:payload_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd', data: { alerts: alerts }).as_payload } + let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) } + + describe '#as_payload' do + it 'only returns id and endpoint' do + expect(payload_no_alerts.keys).to eq [:id, :endpoint] + end + + it 'returns alerts if set' do + expect(payload_alerts.keys).to eq [:id, :endpoint, :alerts] + end + end + + describe '#pushable?' do + it 'obeys alert settings' do + expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true + expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Status'))).to eq false + expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Follow'))).to eq true + expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'FollowRequest'))).to eq false + expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Favourite'))).to eq true + end + end +end diff --git a/yarn.lock b/yarn.lock index 13c3f4951..812a0721a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2209,7 +2209,7 @@ deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" -deep-extend@~0.4.0: +deep-extend@^0.4.0, deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -2416,7 +2416,7 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@^2.5.6: +ejs@^2.3.4, ejs@^2.5.6: version "2.5.6" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" @@ -4059,6 +4059,15 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" +loader-utils@0.2.x: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" @@ -4419,7 +4428,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -4760,6 +4769,16 @@ obuf@^1.0.0, obuf@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" +offline-plugin@^4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/offline-plugin/-/offline-plugin-4.8.3.tgz#9e95bd342ea2ac836b001b81f204c40638694d6c" + dependencies: + deep-extend "^0.4.0" + ejs "^2.3.4" + loader-utils "0.2.x" + minimatch "^3.0.3" + slash "^1.0.0" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" -- cgit From a9a0c854e1df8fbc682eeb059fc68e8dbdbb5bde Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Thu, 13 Jul 2017 22:18:18 +0200 Subject: fix(components/media_modal): Style issues (#4187) --- .../mastodon/components/extended_video_player.js | 4 +++- .../mastodon/features/ui/components/media_modal.js | 20 ++++++++++---------- app/javascript/styles/components.scss | 8 ++++++++ 3 files changed, 21 insertions(+), 11 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js index 4c62fa7b3..b38a4b8ff 100644 --- a/app/javascript/mastodon/components/extended_video_player.js +++ b/app/javascript/mastodon/components/extended_video_player.js @@ -5,6 +5,8 @@ export default class ExtendedVideoPlayer extends React.PureComponent { static propTypes = { src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, time: PropTypes.number, controls: PropTypes.bool.isRequired, muted: PropTypes.bool.isRequired, @@ -30,7 +32,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent { render () { return ( -
+
; } - if (attachment.get('type') === 'image') { - content = media.map((image) => { - const width = image.getIn(['meta', 'original', 'width']) || null; - const height = image.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; + if (image.get('type') === 'image') { return ; - }).toArray(); - } else if (attachment.get('type') === 'gifv') { - content = ; - } + } else if (image.get('type') === 'gifv') { + return ; + } + + return null; + }).toArray(); return (
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 02602afa4..bcf7ba4e0 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2957,6 +2957,7 @@ button.icon-button.active i.fa-retweet { max-height: 80vh; position: relative; + .extended-video-player, img, canvas, video { @@ -2966,6 +2967,13 @@ button.icon-button.active i.fa-retweet { height: auto; } + .extended-video-player, + video { + display: flex; + width: 80vw; + height: 80vh; + } + img, canvas { display: block; -- cgit From a9067167bb368b1692bdd1ceea216b9215275ee2 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 00:49:01 +0200 Subject: Improve swiping (#4188) * feat(components/columns_area): Toggle animation settings * fix(components/media_modal): Center non-visible views * fix(components/media_modal): Check for null * refactor(columns_area): Better logic --- .../mastodon/features/ui/components/columns_area.js | 20 ++++++++++++++------ app/javascript/styles/components.scss | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index cbc185a7d..ef9a15522 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -32,12 +32,19 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; + componentDidUpdate() { + this.lastIndex = getIndex(this.context.router.history.location.pathname); + } + handleSwipe = (index) => { - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - this.context.router.history.push(getLink(index)); - }); - }); + this.pendingIndex = index; + } + + handleAnimationEnd = () => { + if (typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } renderView = (link, index) => { @@ -68,10 +75,11 @@ export default class ColumnsArea extends ImmutablePureComponent { const { columns, children, singleColumn } = this.props; const columnIndex = getIndex(this.context.router.history.location.pathname); + const shouldAnimate = Math.abs(this.lastIndex - columnIndex) === 1; if (singleColumn) { return columnIndex !== -1 ? ( - + {links.map(this.renderView)} ) :
{children}
; diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index bcf7ba4e0..0face646d 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1312,6 +1312,8 @@ } .react-swipeable-view-container > * { + display: flex; + align-items: center; height: 100%; } -- cgit From eeb5923e89c1b9040df37db8709e3450713c6019 Mon Sep 17 00:00:00 2001 From: unarist Date: Fri, 14 Jul 2017 08:59:34 +0900 Subject: Add object-fit polyfill for Edge (#4182) --- .postcssrc.yml | 1 + app/javascript/mastodon/extra_polyfills.js | 3 ++ app/javascript/mastodon/load_polyfills.js | 5 ++- package.json | 2 + yarn.lock | 66 +++++++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 3 deletions(-) (limited to 'app/javascript') diff --git a/.postcssrc.yml b/.postcssrc.yml index 220fe0bb9..efffb39ba 100644 --- a/.postcssrc.yml +++ b/.postcssrc.yml @@ -6,3 +6,4 @@ plugins: - last 2 versions - IE >= 11 - iOS >= 9 + postcss-object-fit-images: {} diff --git a/app/javascript/mastodon/extra_polyfills.js b/app/javascript/mastodon/extra_polyfills.js index 546b693b1..3acc55abd 100644 --- a/app/javascript/mastodon/extra_polyfills.js +++ b/app/javascript/mastodon/extra_polyfills.js @@ -1,2 +1,5 @@ import 'intersection-observer'; import 'requestidlecallback'; +import objectFitImages from 'object-fit-images'; + +objectFitImages(); diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index bc5468595..df7889118 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/app/javascript/mastodon/load_polyfills.js @@ -20,11 +20,12 @@ function loadPolyfills() { ); // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback. + // Edge does not have requestIdleCallback and object-fit CSS property. // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( window.IntersectionObserver && - window.requestIdleCallback + window.requestIdleCallback && + 'object-fit' in (new Image()).style ); return Promise.all([ diff --git a/package.json b/package.json index 1aaa243c8..5ad576dad 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,12 @@ "node-sass": "^4.5.2", "npmlog": "^4.1.2", "object-assign": "^4.1.1", + "object-fit-images": "^3.2.3", "offline-plugin": "^4.8.3", "path-complete-extname": "^0.1.0", "pg": "^6.4.0", "postcss-loader": "^2.0.6", + "postcss-object-fit-images": "^1.1.2", "postcss-smart-import": "^0.7.5", "precss": "^2.0.0", "prop-types": "^15.5.10", diff --git a/yarn.lock b/yarn.lock index 812a0721a..56a9f7798 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2030,12 +2030,38 @@ css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" +css-font-size-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz#854875ace9aca6a8d2ee0d345a44aae9bb6db6cb" + +css-font-stretch-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz#50cee9b9ba031fb5c952d4723139f1e107b54b10" + +css-font-style-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz#5c3532813f63b4a1de954d13cea86ab4333409e4" + +css-font-weight-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz#9bc04671ac85bc724b574ef5d3ac96b0d604fd97" + +css-global-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-global-keywords/-/css-global-keywords-1.0.1.tgz#72a9aea72796d019b1d2a3252de4e5aaa37e4a69" + css-in-js-utils@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-1.0.3.tgz#9ac7e02f763cf85d94017666565ed68a5b5f3215" dependencies: hyphenate-style-name "^1.0.2" +css-list-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-list-helpers/-/css-list-helpers-1.0.1.tgz#fff57192202db83240c41686f919e449a7024f7d" + dependencies: + tcomb "^2.5.0" + css-loader@^0.28.1, css-loader@^0.28.4: version "0.28.4" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.4.tgz#6cf3579192ce355e8b38d5f42dd7a1f2ec898d0f" @@ -2072,6 +2098,10 @@ css-selector-tokenizer@^0.7.0: fastparse "^1.1.1" regexpu-core "^1.0.0" +css-system-font-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz#85c6f086aba4eb32c571a3086affc434b84823ed" + css-what@2.1: version "2.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" @@ -4713,6 +4743,10 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-fit-images@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/object-fit-images/-/object-fit-images-3.2.3.tgz#4089f6d0070a3b5563d3c1ab6f1b28d61331f0ac" + object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" @@ -4896,6 +4930,20 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-css-font@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/parse-css-font/-/parse-css-font-2.0.2.tgz#7b60b060705a25a9b90b7f0ed493e5823248a652" + dependencies: + css-font-size-keywords "^1.0.0" + css-font-stretch-keywords "^1.0.1" + css-font-style-keywords "^1.0.1" + css-font-weight-keywords "^1.0.0" + css-global-keywords "^1.0.1" + css-list-helpers "^1.0.1" + css-system-font-keywords "^1.0.0" + tcomb "^2.5.0" + unquote "^1.1.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -5381,6 +5429,14 @@ postcss-normalize-url@^3.0.7: postcss "^5.0.14" postcss-value-parser "^3.2.3" +postcss-object-fit-images@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/postcss-object-fit-images/-/postcss-object-fit-images-1.1.2.tgz#8b773043db14672ef6cd6f2cb1f0d8b26a9f573b" + dependencies: + parse-css-font "^2.0.2" + postcss "^5.0.16" + quote "^0.4.0" + postcss-ordered-values@^2.1.0: version "2.2.3" resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" @@ -5694,6 +5750,10 @@ querystringify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" +quote@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01" + raf@^3.1.0: version "3.3.2" resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27" @@ -6973,7 +7033,7 @@ tar@^2.0.0, tar@^2.2.1: fstream "^1.0.2" inherits "2" -tcomb@^2.5.1: +tcomb@^2.5.0, tcomb@^2.5.1: version "2.7.0" resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" @@ -7147,6 +7207,10 @@ unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +unquote@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.0.tgz#98e1fc608b6b854c75afb1b95afc099ba69d942f" + urix@^0.1.0, urix@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -- cgit From 87b96f8d339ff7998fc9a357f979f86cc38ad33e Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 14 Jul 2017 18:03:01 +0900 Subject: Add Japanese translations for #3243 (#4192) --- app/javascript/mastodon/locales/ar.json | 3 +++ app/javascript/mastodon/locales/bg.json | 3 +++ app/javascript/mastodon/locales/ca.json | 3 +++ app/javascript/mastodon/locales/de.json | 3 +++ app/javascript/mastodon/locales/defaultMessages.json | 17 +++++++++++++++++ app/javascript/mastodon/locales/en.json | 3 +++ app/javascript/mastodon/locales/eo.json | 3 +++ app/javascript/mastodon/locales/es.json | 3 +++ app/javascript/mastodon/locales/fa.json | 3 +++ app/javascript/mastodon/locales/fi.json | 3 +++ app/javascript/mastodon/locales/fr.json | 3 +++ app/javascript/mastodon/locales/he.json | 3 +++ app/javascript/mastodon/locales/hr.json | 3 +++ app/javascript/mastodon/locales/hu.json | 3 +++ app/javascript/mastodon/locales/id.json | 3 +++ app/javascript/mastodon/locales/io.json | 3 +++ app/javascript/mastodon/locales/it.json | 3 +++ app/javascript/mastodon/locales/ja.json | 3 +++ app/javascript/mastodon/locales/ko.json | 3 +++ app/javascript/mastodon/locales/nl.json | 3 +++ app/javascript/mastodon/locales/no.json | 3 +++ app/javascript/mastodon/locales/oc.json | 3 +++ app/javascript/mastodon/locales/pl.json | 3 +++ app/javascript/mastodon/locales/pt-BR.json | 3 +++ app/javascript/mastodon/locales/pt.json | 3 +++ app/javascript/mastodon/locales/ru.json | 3 +++ app/javascript/mastodon/locales/th.json | 3 +++ app/javascript/mastodon/locales/tr.json | 3 +++ app/javascript/mastodon/locales/uk.json | 3 +++ app/javascript/mastodon/locales/zh-CN.json | 3 +++ app/javascript/mastodon/locales/zh-HK.json | 3 +++ app/javascript/mastodon/locales/zh-TW.json | 3 +++ config/locales/ja.yml | 15 +++++++++++++++ 33 files changed, 125 insertions(+) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 6992e7e0f..7b890ce64 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "المُفَضَّلة :", "notifications.column_settings.follow": "متابعُون جُدُد :", "notifications.column_settings.mention": "الإشارات :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "الترقيّات:", "notifications.column_settings.show": "إعرِضها في عمود", "notifications.column_settings.sound": "أصدر صوتا", @@ -147,6 +149,7 @@ "report.target": "إبلاغ", "search.placeholder": "ابحث", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "تعذرت ترقية هذا المنشور", "status.delete": "إحذف", "status.favourite": "أضف إلى المفضلة", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 7a56e1446..0cf6bf3ac 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Предпочитани:", "notifications.column_settings.follow": "Нови последователи:", "notifications.column_settings.mention": "Споменавания:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Споделяния:", "notifications.column_settings.show": "Покажи в колона", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Търсене", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Изтриване", "status.favourite": "Предпочитани", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index b2673915a..1e44d6fa5 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorits:", "notifications.column_settings.follow": "Nous seguidors:", "notifications.column_settings.mention": "Mencions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Mostrar en la columna", "notifications.column_settings.sound": "Reproduïr so", @@ -147,6 +149,7 @@ "report.target": "Informes", "search.placeholder": "Cercar", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Aquesta publicació no pot ser retootejada", "status.delete": "Esborrar", "status.favourite": "Favorit", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 4b62403c3..f73011e73 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorisierungen:", "notifications.column_settings.follow": "Neue Folgende:", "notifications.column_settings.mention": "Erwähnungen:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.show": "In der Spalte anzeigen", "notifications.column_settings.sound": "Ton abspielen", @@ -147,6 +149,7 @@ "report.target": "Melden", "search.placeholder": "Suche", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Löschen", "status.favourite": "Favorisieren", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 7c1522299..aaa558c0e 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -861,6 +861,14 @@ "defaultMessage": "Play sound", "id": "notifications.column_settings.sound" }, + { + "defaultMessage": "Push notifications", + "id": "notifications.column_settings.push" + }, + { + "defaultMessage": "This device", + "id": "notifications.column_settings.push_meta" + }, { "defaultMessage": "New followers:", "id": "notifications.column_settings.follow" @@ -936,6 +944,15 @@ ], "path": "app/javascript/mastodon/features/public_timeline/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "A look inside...", + "id": "standalone.public_title" + } + ], + "path": "app/javascript/mastodon/features/standalone/public_timeline/index.json" + }, { "descriptors": [ { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 778f33269..15afe2309 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting {target}", "search.placeholder": "Search", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Delete", "status.favourite": "Favourite", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 2648a6840..4f9e26c25 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoroj:", "notifications.column_settings.follow": "Novaj sekvantoj:", "notifications.column_settings.mention": "Mencioj:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolono", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Serĉi", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Forigi", "status.favourite": "Favori", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index c42930380..64ba78716 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Buscar", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Borrar", "status.favourite": "Favorito", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index c9f1888b5..306937cc2 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "پسندیده‌ها:", "notifications.column_settings.follow": "پیگیران تازه:", "notifications.column_settings.mention": "نام‌بردن‌ها:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "بازبوق‌ها:", "notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.sound": "پخش صدا", @@ -147,6 +149,7 @@ "report.target": "گزارش‌دادن", "search.placeholder": "جستجو", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", "status.delete": "پاک‌کردن", "status.favourite": "پسندیدن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index b836d2f5d..1b17fb155 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Tykkäyksiä:", "notifications.column_settings.follow": "Uusia seuraajia:", "notifications.column_settings.mention": "Mainintoja:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Buusteja:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Hae", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Poista", "status.favourite": "Tykkää", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index eaa01638c..ea69532dd 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", @@ -147,6 +149,7 @@ "report.target": "Signalement", "search.placeholder": "Rechercher", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Cette publication ne peut être boostée", "status.delete": "Effacer", "status.favourite": "Ajouter aux favoris", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 98c7ea021..8b63bd26b 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "מחובבים:", "notifications.column_settings.follow": "עוקבים חדשים:", "notifications.column_settings.mention": "פניות:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "הדהודים:", "notifications.column_settings.show": "הצגה בטור", "notifications.column_settings.sound": "שמע מופעל", @@ -147,6 +149,7 @@ "report.target": "דיווח", "search.placeholder": "חיפוש", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "לא ניתן להדהד הודעה זו", "status.delete": "מחיקה", "status.favourite": "חיבוב", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index fdf5c11c0..165e3088f 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoriti:", "notifications.column_settings.follow": "Novi sljedbenici:", "notifications.column_settings.mention": "Spominjanja:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Prikaži u stupcu", "notifications.column_settings.sound": "Sviraj zvuk", @@ -147,6 +149,7 @@ "report.target": "Prijavljivanje", "search.placeholder": "Traži", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Ovaj post ne može biti podignut", "status.delete": "Obriši", "status.favourite": "Označi omiljenim", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index baf762c8d..71dcce505 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Keresés", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Törlés", "status.favourite": "Kedvenc", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 6f6d688e9..0c21877d8 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorit:", "notifications.column_settings.follow": "Pengikut baru:", "notifications.column_settings.mention": "Balasan:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boost:", "notifications.column_settings.show": "Tampilkan dalam kolom", "notifications.column_settings.sound": "Mainkan suara", @@ -147,6 +149,7 @@ "report.target": "Melaporkan", "search.placeholder": "Pencarian", "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Hapus", "status.favourite": "Difavoritkan", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 25e0adc8a..788d09f34 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorati:", "notifications.column_settings.follow": "Nova sequanti:", "notifications.column_settings.mention": "Mencioni:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Repeti:", "notifications.column_settings.show": "Montrar en kolumno", "notifications.column_settings.sound": "Plear sono", @@ -147,6 +149,7 @@ "report.target": "Denuncante", "search.placeholder": "Serchez", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Efacar", "status.favourite": "Favorizar", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 4881b0f08..9176bfaaf 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Apprezzati:", "notifications.column_settings.follow": "Nuovi seguaci:", "notifications.column_settings.mention": "Menzioni:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", "notifications.column_settings.sound": "Riproduci suono", @@ -147,6 +149,7 @@ "report.target": "Invio la segnalazione", "search.placeholder": "Cerca", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Elimina", "status.favourite": "Apprezzato", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index f62072852..a686cdc03 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "お気に入り", "notifications.column_settings.follow": "新しいフォロワー", "notifications.column_settings.mention": "返信", + "notifications.column_settings.push": "プッシュ通知", + "notifications.column_settings.push_meta": "このデバイス", "notifications.column_settings.reblog": "ブースト", "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", @@ -147,6 +149,7 @@ "report.target": "問題のユーザー", "search.placeholder": "検索", "search_results.total": "{count, number}件の結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "この投稿はブーストできません", "status.delete": "削除", "status.favourite": "お気に入り", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 5e1aaac85..0b47cc990 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "즐겨찾기", "notifications.column_settings.follow": "새 팔로워", "notifications.column_settings.mention": "답글", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "부스트", "notifications.column_settings.show": "컬럼에 표시", "notifications.column_settings.sound": "효과음 재생", @@ -147,6 +149,7 @@ "report.target": "문제가 된 사용자", "search.placeholder": "검색", "search_results.total": "{count, number}건의 결과", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", "status.delete": "삭제", "status.favourite": "즐겨찾기", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 479d157f3..cf6a8bd31 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorieten:", "notifications.column_settings.follow": "Nieuwe volgers:", "notifications.column_settings.mention": "Vermeldingen:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.sound": "Geluid afspelen", @@ -147,6 +149,7 @@ "report.target": "Rapporteren van", "search.placeholder": "Zoeken", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Deze toot kan niet geboost worden", "status.delete": "Verwijderen", "status.favourite": "Favoriet", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 4bbf14938..1f4082d7b 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Likt:", "notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.mention": "Nevnt:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Fremhevet:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Spill lyd", @@ -147,6 +149,7 @@ "report.target": "Rapporterer", "search.placeholder": "Søk", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Denne posten kan ikke fremheves", "status.delete": "Slett", "status.favourite": "Lik", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 2c119ef41..dc6dd5e32 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.follow": "Nòus seguidors :", "notifications.column_settings.mention": "Mencions :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partatges :", "notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.sound": "Emetre un son", @@ -147,6 +149,7 @@ "report.target": "Senhalar {target}", "search.placeholder": "Recercar", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", "status.delete": "Escafar", "status.favourite": "Apondre als favorits", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index c2288c4c0..233d61995 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Ulubione:", "notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.mention": "Wspomniali:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Podbili:", "notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.sound": "Odtwarzaj dźwięk", @@ -147,6 +149,7 @@ "report.target": "Zgłaszanie {target}", "search.placeholder": "Szukaj", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Ten post nie może zostać podbity", "status.delete": "Usuń", "status.favourite": "Ulubione", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index b199a39ce..cf2b911f2 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", @@ -147,6 +149,7 @@ "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Eliminar", "status.favourite": "Adicionar aos favoritos", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index b199a39ce..cf2b911f2 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", @@ -147,6 +149,7 @@ "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Eliminar", "status.favourite": "Adicionar aos favoritos", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index f9f48a48d..942a13ede 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Нравится:", "notifications.column_settings.follow": "Новые подписчики:", "notifications.column_settings.mention": "Упоминания:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Продвижения:", "notifications.column_settings.show": "Показывать в колонке", "notifications.column_settings.sound": "Проигрывать звук", @@ -147,6 +149,7 @@ "report.target": "Жалуемся на", "search.placeholder": "Поиск", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Этот статус не может быть продвинут", "status.delete": "Удалить", "status.favourite": "Нравится", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 8a39beacb..e9e96c14f 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Search", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Delete", "status.favourite": "Favourite", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 203e4a09e..adfa79cd9 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoriler:", "notifications.column_settings.follow": "Yeni takipçiler:", "notifications.column_settings.mention": "Bahsedilenler:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boost’lar:", "notifications.column_settings.show": "Bildirimlerde göster", "notifications.column_settings.sound": "Ses çal", @@ -147,6 +149,7 @@ "report.target": "Raporlama", "search.placeholder": "Ara", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Bu gönderi boost edilemez", "status.delete": "Sil", "status.favourite": "Favorilere ekle", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index c0f4a8dbb..435067281 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Вподобане:", "notifications.column_settings.follow": "Нові підписники:", "notifications.column_settings.mention": "Сповіщення:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Передмухи:", "notifications.column_settings.show": "Показати в колонці", "notifications.column_settings.sound": "Відтворювати звук", @@ -147,6 +149,7 @@ "report.target": "Скаржимося на", "search.placeholder": "Пошук", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Цей допис не може бути передмухнутий", "status.delete": "Видалити", "status.favourite": "Подобається", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 998e1c8da..0f2c1fcec 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "你的嘟文被赞:", "notifications.column_settings.follow": "关注你:", "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "搜索", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "没法转嘟这条嘟文啦……", "status.delete": "删除", "status.favourite": "赞", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 1079d5429..c0b4cfce9 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "喜歡你的文章:", "notifications.column_settings.follow": "關注你:", "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "舉報", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "這篇文章無法被轉推", "status.delete": "刪除", "status.favourite": "喜歡", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 6240b8879..772cc691c 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "最愛:", "notifications.column_settings.follow": "新的關注者:", "notifications.column_settings.mention": "提到:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "通報中", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "此貼文無法轉推", "status.delete": "刪除", "status.favourite": "喜愛", diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 7cd97a042..37d82a205 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -320,6 +320,21 @@ ja: next: 次 prev: 前 truncate: "…" + push_notifications: + favourite: + title: あなたのトゥートが %{name} さんにお気に入り登録されました + follow: + title: '%{name} さんにフォローされました' + mention: + action_boost: ブースト + action_expand: もっと見る + action_favourite: お気に入り + title: '%{name} さんから返信がありました' + reblog: + title: あなたのトゥートが %{name} さんにブーストされました + subscribed: + body: あなたはプッシュ通知を受け取ることが出来ます + title: Subscription が登録されました remote_follow: acct: あなたの ユーザー名@ドメイン を入力してください missing_resource: リダイレクト先が見つかりませんでした -- cgit From 9008ab340712fafa6c26862138f5acbbc958bc9a Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 14 Jul 2017 18:08:56 +0900 Subject: Do not load unnecessary script files (#4193) --- app/javascript/mastodon/main.js | 12 +++--------- app/javascript/mastodon/ready.js | 7 +++++++ app/javascript/packs/about.js | 24 ++++++++++++++++++++++++ app/javascript/packs/public.js | 17 ++--------------- app/views/about/show.html.haml | 7 +++---- 5 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 app/javascript/mastodon/ready.js create mode 100644 app/javascript/packs/about.js (limited to 'app/javascript') diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index d2c9d1c94..1f8ebb721 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,12 +1,6 @@ -const perf = require('./performance'); +import ready from './ready'; -function onDomContentLoaded(callback) { - if (document.readyState !== 'loading') { - callback(); - } else { - document.addEventListener('DOMContentLoaded', callback); - } -} +const perf = require('./performance'); function main() { perf.start('main()'); @@ -24,7 +18,7 @@ function main() { } } - onDomContentLoaded(() => { + ready(() => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); diff --git a/app/javascript/mastodon/ready.js b/app/javascript/mastodon/ready.js new file mode 100644 index 000000000..dd543910b --- /dev/null +++ b/app/javascript/mastodon/ready.js @@ -0,0 +1,7 @@ +export default function ready(loaded) { + if (['interactive', 'complete'].includes(document.readyState)) { + loaded(); + } else { + document.addEventListener('DOMContentLoaded', loaded); + } +} diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js new file mode 100644 index 000000000..7b8ab5e5d --- /dev/null +++ b/app/javascript/packs/about.js @@ -0,0 +1,24 @@ +import TimelineContainer from '../mastodon/containers/timeline_container'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import loadPolyfills from '../mastodon/load_polyfills'; +import ready from '../mastodon/ready'; + +require.context('../images/', true); + +function loaded() { + const mountNode = document.getElementById('mastodon-timeline'); + + if (mountNode !== null) { + const props = JSON.parse(mountNode.getAttribute('data-props')); + ReactDOM.render(, mountNode); + } +} + +function main() { + ready(loaded); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 0b00da39d..6e6ba3476 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -4,9 +4,7 @@ import { delegate } from 'rails-ujs'; import emojify from '../mastodon/emoji'; import { getLocale } from '../mastodon/locales'; import loadPolyfills from '../mastodon/load_polyfills'; -import TimelineContainer from '../mastodon/containers/timeline_container'; -import React from 'react'; -import ReactDOM from 'react-dom'; +import ready from '../mastodon/ready'; require.context('../images/', true); @@ -39,21 +37,10 @@ function loaded() { const datetime = new Date(content.getAttribute('datetime')); content.textContent = relativeFormat.format(datetime);; }); - - const mountNode = document.getElementById('mastodon-timeline'); - - if (mountNode !== null) { - const props = JSON.parse(mountNode.getAttribute('data-props')); - ReactDOM.render(, mountNode); - } } function main() { - if (['interactive', 'complete'].includes(document.readyState)) { - loaded(); - } else { - document.addEventListener('DOMContentLoaded', loaded); - } + ready(loaded); delegate(document, '.video-player video', 'click', ({ target }) => { if (target.paused) { diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index f75f87c99..fd468bba0 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -1,11 +1,10 @@ -- content_for :header_tags do - %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - content_for :page_title do = site_hostname - content_for :header_tags do + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) + = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' + %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:url', content: about_url }/ %meta{ property: 'og:type', content: 'website' }/ -- cgit From 489d1624777d86b710bd4f467857a7e7c3bd3128 Mon Sep 17 00:00:00 2001 From: Albert ARIBAUD Date: Fri, 14 Jul 2017 12:12:16 +0200 Subject: fr.json: replace "silencer" with "masquer" (#4196) "Silencer" as a verb does not exist in French. A good and valid replacement is "masquer". --- app/javascript/mastodon/locales/fr.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index ea69532dd..b6605295b 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -29,7 +29,7 @@ "column.favourites": "Favoris", "column.follow_requests": "Demandes de suivi", "column.home": "Accueil", - "column.mutes": "Comptes silencés", + "column.mutes": "Comptes masqués", "column.notifications": "Notifications", "column.public": "Fil public global", "column_back_button.label": "Retour", @@ -52,9 +52,9 @@ "confirmations.delete.confirm": "Supprimer", "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?", "confirmations.domain_block.confirm": "Masquer le domaine entier", - "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.", - "confirmations.mute.confirm": "Silencer", - "confirmations.mute.message": "Confirmez vous la silenciation {name} ?", + "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.", + "confirmations.mute.confirm": "Masquer", + "confirmations.mute.message": "Confirmez vous le masquage de {name} ?", "emoji_button.activity": "Activités", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Boire et manger", @@ -96,7 +96,7 @@ "navigation_bar.follow_requests": "Demandes de suivi", "navigation_bar.info": "Plus d’informations", "navigation_bar.logout": "Déconnexion", - "navigation_bar.mutes": "Comptes silencés", + "navigation_bar.mutes": "Comptes masqués", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", "notification.favourite": "{name} a ajouté à ses favoris :", -- cgit From e2685ccc81f04e1a63a97af80686bf85027418a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 19:47:53 +0200 Subject: Fix #4149, fix #1199 - Store emojis as unicode (#4189) - Use unicode when selecting emoji through picker - Convert shortcodes to unicode when storing text input server-side - Do not convert shortcodes in JS anymore --- Gemfile | 1 + Gemfile.lock | 4 ++- app/helpers/emoji_helper.rb | 19 ++++++++++++ app/javascript/mastodon/actions/compose.js | 7 +++-- app/javascript/mastodon/emoji.js | 24 ++------------- .../features/compose/components/compose_form.js | 3 +- .../compose/components/emoji_picker_dropdown.js | 5 +-- app/javascript/mastodon/reducers/compose.js | 2 +- app/javascript/styles/components.scss | 1 + app/models/account.rb | 10 ++++++ app/models/status.rb | 6 +++- app/services/post_status_service.rb | 2 ++ spec/helpers/emoji_helper_spec.rb | 15 +++++++++ spec/helpers/routing_helper.rb | 5 --- spec/javascript/components/emojify.test.js | 36 ---------------------- 15 files changed, 69 insertions(+), 71 deletions(-) create mode 100644 app/helpers/emoji_helper.rb create mode 100644 spec/helpers/emoji_helper_spec.rb delete mode 100644 spec/helpers/routing_helper.rb (limited to 'app/javascript') diff --git a/Gemfile b/Gemfile index 988b4d6b9..531d01ae0 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,7 @@ gem 'devise', '~> 4.2' gem 'devise-two-factor', '~> 3.0' gem 'doorkeeper', '~> 4.2' gem 'fast_blank', '~> 1.0' +gem 'gemoji', '~> 3.0' gem 'goldfinger', '~> 1.2' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.5' diff --git a/Gemfile.lock b/Gemfile.lock index 5599e1db1..83202189d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,9 +106,9 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - charlock_holmes (0.7.3) case_transform (0.2) activesupport + charlock_holmes (0.7.3) chunky_png (1.3.8) cld3 (3.1.3) ffi (>= 1.1.0, < 1.10.0) @@ -163,6 +163,7 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) + gemoji (3.0.0) globalid (0.4.0) activesupport (>= 4.2.0) goldfinger (1.2.0) @@ -518,6 +519,7 @@ DEPENDENCIES faker (~> 1.7) fast_blank (~> 1.0) fuubar (~> 2.2) + gemoji (~> 3.0) goldfinger (~> 1.2) hamlit-rails (~> 0.2) hiredis (~> 0.6) diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb new file mode 100644 index 000000000..c1595851f --- /dev/null +++ b/app/helpers/emoji_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module EmojiHelper + EMOJI_PATTERN = /(?<=[^[:alnum:]:]|\n|^):([\w+-]+):(?=[^[:alnum:]:]|$)/x + + def emojify(text) + return text if text.blank? + + text.gsub(EMOJI_PATTERN) do |match| + emoji = Emoji.find_by_alias($1) # rubocop:disable Rails/DynamicFindBy,Style/PerlBackrefs + + if emoji + emoji.raw + else + match + end + end + end +end diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 647a52b93..9f05a53e9 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -2,8 +2,6 @@ import api from '../api'; import { updateTimeline } from './timelines'; -import * as emojione from 'emojione'; - export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; @@ -73,11 +71,14 @@ export function mentionCompose(account, router) { export function submitCompose() { return function (dispatch, getState) { - const status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')); + const status = getState().getIn(['compose', 'text'], ''); + if (!status || !status.length) { return; } + dispatch(submitComposeRequest()); + api(getState).post('/api/v1/statuses', { status, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 7043d5f3a..ed2180cd1 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -6,36 +6,18 @@ const trie = new Trie(Object.keys(emojione.jsEscapeMap)); function emojify(str) { // This walks through the string from start to end, ignoring any tags (

,
, etc.) - // and replacing valid shortnames like :smile: and :wink: as well as unicode strings + // and replacing valid unicode strings // that _aren't_ within tags with an version. - // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster. + // The goal is to be the same as an emojione.regUnicode replacement, but faster. let i = -1; let insideTag = false; - let insideShortname = false; - let shortnameStartIndex = -1; let match; while (++i < str.length) { const char = str.charAt(i); - if (insideShortname && char === ':') { - const shortname = str.substring(shortnameStartIndex, i + 1); - if (shortname in emojione.emojioneList) { - const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; - const alt = emojione.convert(unicode.toUpperCase()); - const replacement = `${alt}`; - str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); - i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string - } else { - i--; // stray colon, try again - } - insideShortname = false; - } else if (insideTag && char === '>') { + if (insideTag && char === '>') { insideTag = false; } else if (char === '<') { insideTag = true; - insideShortname = false; - } else if (!insideTag && char === ':') { - insideShortname = true; - shortnameStartIndex = i; } else if (!insideTag && (match = trie.search(str.substring(i)))) { const unicodeStr = match; if (unicodeStr in emojione.jsEscapeMap) { diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index f7eeedc69..f07552947 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -136,7 +136,8 @@ export default class ComposeForm extends ImmutablePureComponent { handleEmojiPick = (data) => { const position = this.autosuggestTextarea.textarea.selectionStart; - this._restoreCaret = position + data.shortname.length + 1; + const emojiChar = String.fromCodePoint(parseInt(data.unicode, 16)); + this._restoreCaret = position + emojiChar.length + 1; this.props.onPickEmoji(position, data); } 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 83c66a5d5..acc584f20 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -109,11 +109,12 @@ export default class EmojiPickerDropdown extends React.PureComponent { 🙂 + { this.state.active && !this.state.loading && diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index a92b5aa23..ea3b78b67 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -118,7 +118,7 @@ const insertSuggestion = (state, position, token, completion) => { }; const insertEmoji = (state, position, emojiData) => { - const emoji = emojiData.shortname; + const emoji = String.fromCodePoint(parseInt(emojiData.unicode, 16)); return state.withMutations(map => { map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`); diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 0face646d..0420a2bed 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2708,6 +2708,7 @@ button.icon-button.active i.fa-retweet { margin-left: 2px; width: 24px; outline: 0; + cursor: pointer; &:active, &:focus { diff --git a/app/models/account.rb b/app/models/account.rb index 2b54cee5f..7243cb1a5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -47,6 +47,7 @@ class Account < ApplicationRecord include AccountInteractions include Attachmentable include Remotable + include EmojiHelper # Local users has_one :user, inverse_of: :account @@ -240,9 +241,18 @@ class Account < ApplicationRecord before_create :generate_keys before_validation :normalize_domain + before_validation :prepare_contents, if: :local? private + def prepare_contents + display_name&.strip! + note&.strip! + + self.display_name = emojify(display_name) + self.note = emojify(note) + end + def generate_keys return unless local? diff --git a/app/models/status.rb b/app/models/status.rb index 65db7579a..24eaf7071 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -29,6 +29,7 @@ class Status < ApplicationRecord include Streamable include Cacheable include StatusThreadingConcern + include EmojiHelper enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility @@ -120,7 +121,7 @@ class Status < ApplicationRecord !sensitive? && media_attachments.any? end - before_validation :prepare_contents + before_validation :prepare_contents, if: :local? before_validation :set_reblog before_validation :set_visibility before_validation :set_conversation @@ -241,6 +242,9 @@ class Status < ApplicationRecord def prepare_contents text&.strip! spoiler_text&.strip! + + self.text = emojify(text) + self.spoiler_text = emojify(spoiler_text) end def set_reblog diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 2e6fbb5c3..951a38e19 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -21,6 +21,7 @@ class PostStatusService < BaseService media = validate_media!(options[:media_ids]) status = nil + ApplicationRecord.transaction do status = account.statuses.create!(text: text, thread: in_reply_to, @@ -31,6 +32,7 @@ class PostStatusService < BaseService application: options[:application]) attach_media(status, media) end + process_mentions_service.call(status) process_hashtags_service.call(status) diff --git a/spec/helpers/emoji_helper_spec.rb b/spec/helpers/emoji_helper_spec.rb new file mode 100644 index 000000000..1eedfb719 --- /dev/null +++ b/spec/helpers/emoji_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe EmojiHelper, type: :helper do + describe '#emojify' do + it 'converts shortcodes to unicode' do + text = ':book: Book' + expect(emojify(text)).to eq '📖 Book' + end + + it 'does not convert shortcodes that are part of a string into unicode' do + text = ':see_no_evil::hear_no_evil::speak_no_evil:' + expect(emojify(text)).to eq text + end + end +end diff --git a/spec/helpers/routing_helper.rb b/spec/helpers/routing_helper.rb deleted file mode 100644 index 3cd397397..000000000 --- a/spec/helpers/routing_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe RoutingHelper, type: :helper do - -end diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js index 3e8b25af9..e165b4519 100644 --- a/spec/javascript/components/emojify.test.js +++ b/spec/javascript/components/emojify.test.js @@ -2,32 +2,6 @@ import { expect } from 'chai'; import emojify from '../../../app/javascript/mastodon/emoji'; describe('emojify', () => { - it('does a basic emojify', () => { - expect(emojify(':smile:')).to.equal( - '😄'); - }); - - it('does a double emojify', () => { - expect(emojify(':smile: and :wink:')).to.equal( - '😄 and 😉'); - }); - - it('works with random colons', () => { - expect(emojify(':smile: : :wink:')).to.equal( - '😄 : 😉'); - expect(emojify(':smile::::wink:')).to.equal( - '😄::😉'); - expect(emojify(':smile:::::wink:')).to.equal( - '😄:::😉'); - }); - - it('works with tags', () => { - expect(emojify('

:smile:

')).to.equal( - '

😄

'); - expect(emojify('

:smile:

and

:wink:

')).to.equal( - '

😄

and

😉

'); - }); - it('ignores unknown shortcodes', () => { expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:'); }); @@ -46,11 +20,6 @@ describe('emojify', () => { expect(emojify(':smile')).to.equal(':smile'); }); - it('does two emoji next to each other', () => { - expect(emojify(':smile::wink:')).to.equal( - '😄😉'); - }); - it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal( '👩‍👩‍👦‍👦'); @@ -72,12 +41,7 @@ describe('emojify', () => { 'foo ❗ #️⃣ bar'); }); - it('does mixed unicode and shortnames', () => { - expect(emojify(':smile:#\uFE0F\u20E3:wink:\u2757')).to.equal('😄#️⃣😉❗'); - }); - it('ignores unicode inside of tags', () => { expect(emojify('

')).to.equal('

'); }); - }); -- cgit From c1f201c49a007e5c0740c00651e549a7b0416b05 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 20:30:12 +0200 Subject: 🎄🔨 Force tree shake emojione (#4202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(yarn): Install babel-plugin-preval as development dependency * feat(babel): Add preval as a plugin * feat(emojione_light): Prevaled module what tree-shaked emojione * refactor(emoji): Use emojione_light * feat: Preload emojione_picker bundle * fix(emojione_light): Do not use Object.entries * fix(emojify): Update tests * chore(emojione_light): Remove silly ascii art --- .babelrc | 3 ++- app/javascript/mastodon/emoji.js | 15 ++++++--------- app/javascript/mastodon/emojione_light.js | 11 +++++++++++ app/views/layouts/application.html.haml | 1 + package.json | 1 + spec/javascript/components/emojify.test.js | 16 ++++++++-------- yarn.lock | 12 ++++++++++-- 7 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 app/javascript/mastodon/emojione_light.js (limited to 'app/javascript') diff --git a/.babelrc b/.babelrc index 19968964e..de922f389 100644 --- a/.babelrc +++ b/.babelrc @@ -22,7 +22,8 @@ { "messagesDir": "./build/messages" } - ] + ], + "preval" ], "env": { "development": { diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index ed2180cd1..1de41f572 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -1,8 +1,7 @@ -import emojione from 'emojione'; +import { unicodeToFilename } from './emojione_light'; import Trie from 'substring-trie'; -const mappedUnicode = emojione.mapUnicodeToShort(); -const trie = new Trie(Object.keys(emojione.jsEscapeMap)); +const trie = new Trie(Object.keys(unicodeToFilename)); function emojify(str) { // This walks through the string from start to end, ignoring any tags (

,
, etc.) @@ -20,12 +19,10 @@ function emojify(str) { insideTag = true; } else if (!insideTag && (match = trie.search(str.substring(i)))) { const unicodeStr = match; - if (unicodeStr in emojione.jsEscapeMap) { - const unicode = emojione.jsEscapeMap[unicodeStr]; - const short = mappedUnicode[unicode]; - const filename = emojione.emojioneList[short].fname; - const alt = emojione.convert(unicode.toUpperCase()); - const replacement = `${alt}`; + if (unicodeStr in unicodeToFilename) { + const filename = unicodeToFilename[unicodeStr]; + const alt = unicodeStr; + const replacement = `${alt}`; str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string } diff --git a/app/javascript/mastodon/emojione_light.js b/app/javascript/mastodon/emojione_light.js new file mode 100644 index 000000000..c75e10a98 --- /dev/null +++ b/app/javascript/mastodon/emojione_light.js @@ -0,0 +1,11 @@ +// @preval +// Force tree shaking on emojione by exposing just a subset of its functionality + +const emojione = require('emojione'); + +const mappedUnicode = emojione.mapUnicodeToShort(); + +module.exports.unicodeToFilename = Object.keys(emojione.jsEscapeMap) + .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]]) + .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: emojione.emojioneList[shortCode].fname })) + .reduce((x, y) => Object.assign(x, y), { }); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index ef97fb127..82b20810a 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -27,6 +27,7 @@ = javascript_pack_tag 'features/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 'emojione_picker', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags diff --git a/package.json b/package.json index 5ad576dad..20224796f 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@storybook/addon-actions": "^3.1.8", "@storybook/react": "^3.1.8", "babel-eslint": "^7.2.3", + "babel-plugin-preval": "^1.3.2", "chai": "^4.1.0", "chai-enzyme": "^0.8.0", "enzyme": "^2.9.1", diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js index e165b4519..2874bb56d 100644 --- a/spec/javascript/components/emojify.test.js +++ b/spec/javascript/components/emojify.test.js @@ -22,23 +22,23 @@ describe('emojify', () => { it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal( - '👩‍👩‍👦‍👦'); + '👩‍👩‍👦‍👦'); expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal( - '👨👩👧👧'); - expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('👩👩👦'); + '👨👩👧👧'); + expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('👩👩👦'); expect(emojify('\u2757')).to.equal( - '❗'); + '❗'); }); it('does multiple unicode', () => { expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal( - '❗ #️⃣'); + '❗ #️⃣'); expect(emojify('\u2757#\uFE0F\u20E3')).to.equal( - '❗#️⃣'); + '❗#️⃣'); expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal( - '❗ #️⃣ ❗'); + '❗ #️⃣ ❗'); expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal( - 'foo ❗ #️⃣ bar'); + 'foo ❗ #️⃣ bar'); }); it('ignores unicode inside of tags', () => { diff --git a/yarn.lock b/yarn.lock index 56a9f7798..defd8599f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -681,6 +681,14 @@ babel-plugin-lodash@^3.2.11: glob "^7.1.1" lodash "^4.17.2" +babel-plugin-preval@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-1.3.2.tgz#44192e6e97b58661bf2c5bcae90bba2a366e0134" + dependencies: + babel-core "^6.25.0" + babylon "^6.17.4" + require-from-string "^1.2.1" + babel-plugin-react-docgen@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-1.5.0.tgz#0339717ad51f4a5ce4349330b8266ea5a56f53b4" @@ -1312,7 +1320,7 @@ babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.25 lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.17.0, babylon@^6.17.2: +babylon@^6.17.0, babylon@^6.17.2, babylon@^6.17.4: version "6.17.4" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" @@ -6335,7 +6343,7 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" -require-from-string@^1.1.0: +require-from-string@^1.1.0, require-from-string@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" -- cgit From 72bd73f605a7253daf445f76425da3182a1f669c Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 22:31:25 +0200 Subject: Small style fixes (#4206) * fix(components/media_modal): Center horizontally in Firefox * fix(components/status_list): Do not remove load more button --- app/javascript/mastodon/components/load_more.js | 9 ++++++++- app/javascript/mastodon/components/status_list.js | 6 +----- app/javascript/styles/components.scss | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js index 2996d4dc8..e2fe1fed7 100644 --- a/app/javascript/mastodon/components/load_more.js +++ b/app/javascript/mastodon/components/load_more.js @@ -6,11 +6,18 @@ export default class LoadMore extends React.PureComponent { static propTypes = { onClick: PropTypes.func, + visible: PropTypes.bool, + } + + static defaultProps = { + visible: true, } render() { + const { visible } = this.props; + return ( - ); diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 5bc46e8ee..86e8386bd 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -101,13 +101,9 @@ export default class StatusList extends ImmutablePureComponent { render () { const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; - let loadMore = null; + const loadMore = 0 && hasMore} onClick={this.handleLoadMore} />; let scrollableArea = null; - if (!isLoading && statusIds.size > 0 && hasMore) { - loadMore = ; - } - if (isLoading || statusIds.size > 0 || !emptyMessage) { scrollableArea = (

diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 0420a2bed..fc797a508 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1314,6 +1314,7 @@ .react-swipeable-view-container > * { display: flex; align-items: center; + justify-content: center; height: 100%; } -- cgit From 3fbf1bf35acf89d1e397fa2e632529bf5105fe02 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 15 Jul 2017 00:49:34 +0200 Subject: Fix #3773 - Pin favourites column (#4201) --- .../mastodon/features/favourited_statuses/index.js | 64 ++++++++++++++++------ .../features/ui/components/columns_area.js | 3 +- 2 files changed, 48 insertions(+), 19 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 8cef6a1e4..d9ad9bc1f 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -2,11 +2,11 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; import Column from '../ui/components/column'; +import ColumnHeader from '../../components/column_header'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import StatusList from '../../components/status_list'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -16,8 +16,6 @@ const messages = defineMessages({ const mapStateToProps = state => ({ statusIds: state.getIn(['status_lists', 'favourites', 'items']), - loaded: state.getIn(['status_lists', 'favourites', 'loaded']), - me: state.getIn(['meta', 'me']), }); @connect(mapStateToProps) @@ -27,34 +25,64 @@ export default class Favourites extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list.isRequired, - loaded: PropTypes.bool, intl: PropTypes.object.isRequired, - me: PropTypes.number.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, }; componentWillMount () { this.props.dispatch(fetchFavouritedStatuses()); } + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('FAVOURITES', {})); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + handleScrollToBottom = () => { this.props.dispatch(expandFavouritedStatuses()); } render () { - const { loaded, intl } = this.props; - - if (!loaded) { - return ( - - - - ); - } + const { intl, statusIds, columnId, multiColumn } = this.props; + const pinned = !!columnId; return ( - - - + + + + ); } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index ef9a15522..ae3f9261b 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -9,7 +9,7 @@ import { links, getIndex, getLink } 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'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; const componentMap = { 'COMPOSE': Compose, @@ -18,6 +18,7 @@ const componentMap = { 'PUBLIC': PublicTimeline, 'COMMUNITY': CommunityTimeline, 'HASHTAG': HashtagTimeline, + 'FAVOURITES': FavouritedStatuses, }; export default class ColumnsArea extends ImmutablePureComponent { -- cgit From 695439775eacea081c7257aabab39d0ec6b492dc Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 16 Jul 2017 00:25:04 +0900 Subject: Fix column swiping (#4211) This fixes broken behavior and enable animation only on swiping. --- .../mastodon/features/ui/components/columns_area.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index ae3f9261b..515c377b9 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -33,8 +33,22 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; + state = { + shouldAnimate: false, + } + + componentWillReceiveProps() { + this.setState({ shouldAnimate: false }); + } + + componentDidMount() { + this.lastIndex = getIndex(this.context.router.history.location.pathname); + this.setState({ shouldAnimate: true }); + } + componentDidUpdate() { this.lastIndex = getIndex(this.context.router.history.location.pathname); + this.setState({ shouldAnimate: true }); } handleSwipe = (index) => { @@ -74,9 +88,10 @@ export default class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn } = this.props; + const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); - const shouldAnimate = Math.abs(this.lastIndex - columnIndex) === 1; + this.pendingIndex = null; if (singleColumn) { return columnIndex !== -1 ? ( -- cgit