From f41590912d5f059a286c2eaa0109ef21747d8b43 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 12 Jul 2017 21:49:17 +0900 Subject: [PATCH 01/35] Fix broken terms for Japanese (#4170) --- config/locales/ja.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index d57fe8da264..133db1b95ce 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -402,7 +402,7 @@ ja:

このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。

-

データ保持のポリシーはどのようになっていますか?

このサービスはデータ保持に関して次のことを行うよう努めます。:

From 43f868de3dd40b30e0d11fbf74452d6253a649f7 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 12 Jul 2017 21:50:07 +0900 Subject: [PATCH 02/35] Add Japanese translations for #4163 and #4129 (#4166) * Add Japanese translations for #4163 * Add Japanese translations for #4129 * top page -> front page --- config/locales/ja.yml | 33 +++++++++++++++++++++---------- config/locales/simple_form.en.yml | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 133db1b95ce..7cd97a0429b 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -154,24 +154,31 @@ ja: view: 表示 settings: contact_information: - email: 公開するメールアドレスを入力 - username: ユーザー名を入力 + email: ビジネスメールアドレス + username: 連絡先のユーザー名 registrations: closed_message: - desc_html: 新規登録を停止しているときにフロントページに表示されます。
HTMLタグが利用可能です。 + desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます title: 新規登録停止時のメッセージ + deletion: + desc_html: 誰でも自分のアカウントを削除できるようにします + title: アカウント削除を受け付ける open: + desc_html: 誰でも自由にアカウントを作成できるようにします title: 新規登録を受け付ける site_description: - desc_html: トップページへの表示と meta タグに使用されます。
HTMLタグ、特に<a><em>が利用可能です。 - title: サイトの説明文 + desc_html: フロントページへの表示と meta タグに使用される紹介文です。HTMLタグ、特に<a><em>が使えます。 + title: インスタンスの説明 site_description_extended: - desc_html: インスタンスについてのページに表示されます。
HTMLタグが利用可能です。 - title: サイトの詳細な説明 + desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます + title: カスタム詳細説明 site_terms: - desc_html: プライバシーポリシーのページに表示されます。
HTMLタグが利用可能です。 - title: サイトのプライバシーポリシー - site_title: サイトのタイトル + desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます + title: カスタム利用規約 + site_title: インスタンスの名前 + timeline_preview: + desc_html: ランディングページに公開タイムラインを表示します + title: タイムラインプレビュー title: サイト設定 subscriptions: callback_url: コールバックURL @@ -206,6 +213,12 @@ ja: authorize_follow: error: 残念ながら、リモートアカウントにエラーが発生しました。 follow: フォロー + follow_request: 'あなたは以下のアカウントにフォローリクエストを送信しました:' + following: '成功! あなたは現在以下のアカウントをフォローしています:' + post_follow: + close: またはこのウィンドウを閉じます + return: ユーザーのプロフィールに戻る + web: Web を開く prompt_html: 'あなた(%{self})は以下のアカウントのフォローをリクエストしました:' title: "%{acct} をフォロー" datetime: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index fc5ab5ec8ca..e3f784a1a78 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -27,6 +27,7 @@ en: data: Data display_name: Display name email: E-mail address + filtered_languages: Filtered languages header: Header locale: Language locked: Lock account From 3567ac3d3efd9efcee770a0c4925fe5c86bb4d4f Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Wed, 12 Jul 2017 15:53:50 +0200 Subject: [PATCH 03/35] 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 ++++++++--------- config/locales/pl.yml | 52 ++++++++++++++++++------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index ac63ec40fe1..c2288c4c07a 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", diff --git a/config/locales/pl.yml b/config/locales/pl.yml index c6588e8464c..dc5aa716bd3 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1,17 +1,30 @@ --- pl: about: - about_mastodon: Mastodon jest wolną i otwartą siecią społecznościową, zdecentralizowaną alternatywą dla zamkniętych, komercyjnych platform. Pozwala uniknąć ryzyka monopolizacji Twojej komunikacji przez jedną korporację. Wybierz serwer, któremu ufasz — nie ograniczy to Twoich możliwości komunikacji z innymi osobami w sieci. Każdy może też uruchomić własną instancję Mastodona i dołączyć do reszty tej sieci społecznościowej. + about_mastodon_html: Mastodon jest wolną i otwartą siecią społecznościową, zdecentralizowaną alternatywą dla zamkniętych, komercyjnych platform. about_this: O tej instancji business_email: 'Służbowy adres e-mail:' - closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta. + closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta. Możesz jednak zarejestrować się na innej instancji, uzyskując dostęp do tej samej sieci. contact: Kontakt description_headline: Czym jest %{domain}? domain_count_after: instancji domain_count_before: Serwer połączony z + features: + humane_approach_body: Nauczeni na błędach innych sieci społecznościowych, Mastodon został zaprojektowany tak, aby uniknąć częstych nadużyć. + humane_approach_title: Bardziej ludzkie podejście + not_a_product_body: Mastodon nie jest komercyjną siecią. Nie doświadczysz tu reklam, zbierania danych, ani centralnego ośrodka, tak jak w przypadku wielu rozwiązań. + not_a_product_title: Jesteś człowiekiem, nie produktem + real_conversation_body: Mając do dyspozycji 500 znaków na post, rozdrobnienie zawartości i ostrzeżenia o multimediach, możesz wyrażać siebie na wszystkie możliwe sposoby. + real_conversation_title: Zaprojektowany do prawdziwych rozmów + within_reach_body: Wiele aplikacji dla Androida, iOS i innych platform dzięki przyjaznemu programistom API sprawia, że możesz utrzymywać kontakt ze znajomymi praktycznie wszędzie. + within_reach_title: Zawsze w Twoim zasięgu + find_another_instance: Znajdź inną instancję + generic_description: "%{domain} jest jednym z serwerów sieci" get_started: Rozpocznijmy! + hosted_on: Mastodon uruchomiony na %{domain} + learn_more: Dowiedz się więcej links: Odnośniki - other_instances: Inne instancje + other_instances: Lista instancji source_code: Kod źródłowy status_count_after: wpisów status_count_before: Są autorami @@ -19,6 +32,7 @@ pl: user_count_after: użytkowników user_count_before: Z serwera korzysta version: Wersja + what_is_mastodon: Czym jest Mastodon? accounts: follow: Śledź followers: Śledzących @@ -154,24 +168,31 @@ pl: view: Wyświetl settings: contact_information: - email: Wprowadź publiczny adres e-mail - username: Wprowadź nazwę użytkownika + email: Służbowy adres e-mail + username: Nazwa użytkownika do kontaktu registrations: closed_message: - desc_html: Wyświetlana na stronie głównej, gdy możliwość otwarej rejestracji
nie jest dostępna. Możesz korzystać z tagów HTML + desc_html: Wyświetlana na stronie głównej, gdy możliwość otwarej rejestracji nie jest dostępna. Możesz korzystać z tagów HTML title: Wiadomość o nieaktywnej rejestracji + deletion: + desc_html: Pozwól każdemu na usunięcie konta + title: Możliwość usunięcia open: + desc_html: Pozwól każdemu na założenie konta title: Otwarta rejestracja site_description: - desc_html: Wyświetlany jako nagłówek na stronie głównej oraz jako meta tag.
Możesz korzystać z tagów HTML, w szczególności z <a> i <em>. - title: Opis strony + desc_html: Akapit wprowadzający, widoczny na stronie głównej i znacznikach meta. Możesz korzystać z tagów HTML, w szczególności <a> i <em>. + title: Opis instancji site_description_extended: - desc_html: Wyświetlany w rozszerzonych informacjach o stronie
Możesz korzystać z tagów HTML - title: Extended site description + desc_html: Dobre miejsce na zasady użytkowania, wprowadzenie i inne rzeczy, które wyróżniają tą instancję. Możesz korzystać z tagów HTML + title: Niestandrdowy opis stronyv site_terms: - desc_html: Wyświetlana na stronie zasad użytkowania
Możesz używać tagów HTML - title: Polityka prywatności strony - site_title: Tytuł strony + desc_html: Miejsce na własną politykę prywatności, zasady użytkowania i inne unormowania prawne. Możesz używać tagów HTML + title: Niestandardowe zasady użytkowania + site_title: Nazwa instancji + timeline_preview: + desc_html: Wyświetlaj publiczną oś czasu na stronie widocznej dla niezalogowanych + title: Podgląd osi czasu title: Ustawienia strony subscriptions: callback_url: URL zwrotny @@ -192,6 +213,7 @@ pl: applications: invalid_url: Ten URL jest nieprawidłowy auth: + agreement_html: Rejestrując się, oświadczasz, że zapoznałeś się z naszymi zasadami użytkowania i polityką prywatności. change_password: Bezpieczeństwo delete_account: Usunięcie konta delete_account_html: Jeżeli chcesz usunąć konto, przejdź tutaj. Otrzymasz prośbę o potwierdzenie. @@ -206,7 +228,7 @@ pl: authorize_follow: error: Niestety, podczas sprawdzania zdalnego konta wystąpił błąd follow: Śledź - follow_request: 'Wysłano prośbę o pozwolenie na obserwację:' + follow_request: 'Wysłano prośbę o pozwolenie na śledzenie:' following: 'Pomyślnie! Od teraz śledzisz:' post_follow: close: Ewentualnie, możesz po prostu zamknąć tą stronę. @@ -261,7 +283,7 @@ pl: one: W trakcie usuwania śledzących z jednej domeny… other: W trakcie usuwania śledzących z %{count} domen… true_privacy_html: Pamiętaj, że rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end. - unlocked_warning_html: Każdy może cię śledzić, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować, kto Cię śledzi. + unlocked_warning_html: Każdy może Cię śledzić, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować, kto Cię śledzi. unlocked_warning_title: Twoje konto nie jest zablokowane generic: changes_saved_msg: Ustawienia zapisane! From c45a75ad3419f15d639138ac34c320323367c870 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 12 Jul 2017 22:55:17 +0900 Subject: [PATCH 04/35] Update dependencies for Node.js (2017-07-12) (#4167) * Update @storybook/addon-actions to version 3.1.8 * Update @storybook/react to version 3.1.8 * Update autoprefixer to version 7.1.2 * Update babel-loader to version 7.1.1 * Update babel-preset-env to version 1.6.0 * Update chai to version 4.1.0 * Update js-yaml to version 3.9.0 * Update jsdom to version 11.1.0 * Update postcss-smart-import to version 0.7.5 * Update sinon to version 2.3.7 * Update substring-trie to version 1.0.1 * Update webpack to version 3.2.0 * Update webpack-manifest-plugin to version 1.1.2 * yarn upgrade * Upgrade extract-text-webpack-plugin to version 3.0.0 * Upgrade intl-relativeformat to version 2.0.0 --- package.json | 30 +-- yarn.lock | 512 ++++++++++++++++++++++++++------------------------- 2 files changed, 273 insertions(+), 269 deletions(-) diff --git a/package.json b/package.json index 4c5a3f1d9dc..004c4d1f5bf 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "private": true, "dependencies": { "array-includes": "^3.0.3", - "autoprefixer": "^7.1.0", + "autoprefixer": "^7.1.2", "axios": "^0.16.2", "babel-core": "^6.25.0", - "babel-loader": "^7.1.0", + "babel-loader": "^7.1.1", "babel-plugin-lodash": "^3.2.11", "babel-plugin-react-intl": "^2.3.1", "babel-plugin-react-transform": "^2.0.2", @@ -37,7 +37,7 @@ "babel-plugin-transform-react-jsx-source": "^6.22.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.6", "babel-plugin-transform-runtime": "^6.23.0", - "babel-preset-env": "^1.5.2", + "babel-preset-env": "^1.6.0", "babel-preset-react": "^6.24.1", "classnames": "^2.2.5", "compression-webpack-plugin": "^0.4.0", @@ -49,7 +49,7 @@ "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", "express": "^4.15.2", - "extract-text-webpack-plugin": "^2.1.2", + "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", "font-awesome": "^4.7.0", "glob": "^7.1.1", @@ -57,9 +57,9 @@ "immutable": "^3.8.1", "intersection-observer": "^0.3.2", "intl": "^1.2.5", - "intl-relativeformat": "^1.3.0", + "intl-relativeformat": "^2.0.0", "is-nan": "^1.2.1", - "js-yaml": "^3.8.4", + "js-yaml": "^3.9.0", "lodash": "^4.17.4", "mark-loader": "^0.1.6", "marky": "^1.2.0", @@ -70,7 +70,7 @@ "path-complete-extname": "^0.1.0", "pg": "^6.4.0", "postcss-loader": "^2.0.6", - "postcss-smart-import": "^0.7.4", + "postcss-smart-import": "^0.7.5", "precss": "^2.0.0", "prop-types": "^15.5.10", "punycode": "^2.1.0", @@ -103,32 +103,32 @@ "sass-loader": "^6.0.6", "stringz": "^0.2.2", "style-loader": "^0.18.2", - "substring-trie": "^1.0.0", + "substring-trie": "^1.0.1", "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^3.1.0", "uws": "^8.14.0", - "webpack": "^3.0.0", + "webpack": "^3.2.0", "webpack-bundle-analyzer": "^2.8.2", - "webpack-manifest-plugin": "^1.1.0", + "webpack-manifest-plugin": "^1.1.2", "webpack-merge": "^4.1.0", "websocket.js": "^0.1.12" }, "devDependencies": { - "@storybook/addon-actions": "^3.1.6", - "@storybook/react": "^3.1.6", + "@storybook/addon-actions": "^3.1.8", + "@storybook/react": "^3.1.8", "babel-eslint": "^7.2.3", - "chai": "^4.0.1", + "chai": "^4.1.0", "chai-enzyme": "^0.8.0", "enzyme": "^2.9.1", "eslint": "^3.19.0", "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.10.3", - "jsdom": "^11.0.0", + "jsdom": "^11.1.0", "mocha": "^3.4.1", "react-intl-translations-manager": "^5.0.0", "react-test-renderer": "^15.6.1", - "sinon": "^2.3.5", + "sinon": "^2.3.7", "webpack-dev-server": "^2.5.1", "yargs": "^8.0.2" }, diff --git a/yarn.lock b/yarn.lock index fba802e0a26..13c3f49518d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@storybook/addon-actions@^3.1.6": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.1.6.tgz#0cbf00ede57ff00d1dfe02e554043d6963940064" +"@storybook/addon-actions@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.1.8.tgz#2b6d7aa97530b19965c1010b822f40b130ebbc4d" dependencies: "@storybook/addons" "^3.1.6" deep-equal "^1.0.1" json-stringify-safe "^5.0.1" prop-types "^15.5.8" - react-inspector "^2.0.0" + react-inspector "^2.1.1" uuid "^3.1.0" "@storybook/addon-links@^3.1.6": @@ -44,11 +44,11 @@ fuse.js "^3.0.1" prop-types "^15.5.9" -"@storybook/react@^3.1.6": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.1.6.tgz#9393bb987ff08ee5f49c4557d12eb84377dee5d2" +"@storybook/react@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.1.8.tgz#4d140c5ae7e9b5eaf627f2071d7324aa38c7af49" dependencies: - "@storybook/addon-actions" "^3.1.6" + "@storybook/addon-actions" "^3.1.8" "@storybook/addon-links" "^3.1.6" "@storybook/addons" "^3.1.6" "@storybook/channel-postmessage" "^3.1.6" @@ -93,7 +93,7 @@ url-loader "^0.5.8" util-deprecate "^1.0.2" uuid "^3.0.1" - webpack "^2.5.1" + webpack "^2.5.1 || ^3.0.0" webpack-dev-middleware "^1.10.2" webpack-hot-middleware "^2.18.0" @@ -122,8 +122,8 @@ redux "^3.6.0" "@types/node@^6.0.46": - version "6.0.78" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470" + version "6.0.80" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.80.tgz#914a75799605b4609bd9a2918c865ba3c4141367" abab@^1.0.3: version "1.0.3" @@ -167,8 +167,8 @@ acorn@^4.0.3, acorn@^4.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" acorn@^5.0.0, acorn@^5.0.1, acorn@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" + version "5.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" adjust-sourcemap-loader@^1.1.0: version "1.1.0" @@ -195,7 +195,7 @@ airbnb-js-shims@^1.1.1: string.prototype.padend "^3.0.0" string.prototype.padstart "^3.0.0" -ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: +ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -211,11 +211,11 @@ ajv@^4.7.0, ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.0.0, ajv@^5.1.5: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486" + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" dependencies: co "^4.6.0" - fast-deep-equal "^0.1.0" + fast-deep-equal "^1.0.0" json-schema-traverse "^0.3.0" json-stable-stringify "^1.0.1" @@ -255,6 +255,12 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.1.0.tgz#09c202d5c917ec23188caa5c9cb9179cd9547750" + dependencies: + color-convert "^1.0.0" + any-promise@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" @@ -300,8 +306,8 @@ arr-diff@^2.0.0: arr-flatten "^1.0.1" arr-flatten@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" array-equal@^1.0.0: version "1.0.0" @@ -352,8 +358,8 @@ arrify@^1.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" asap@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" asn1.js@^4.0.0: version "4.9.1" @@ -409,7 +415,7 @@ async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.1.2, async@^2.1.4, async@^2.1.5: +async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1: version "2.5.0" resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: @@ -434,15 +440,15 @@ autoprefixer@^6.3.1: postcss "^5.2.16" postcss-value-parser "^3.2.3" -autoprefixer@^7.1.0, autoprefixer@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.1.tgz#97bc854c7d0b979f8d6489de547a0d17fb307f6d" +autoprefixer@^7.1.1, autoprefixer@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.2.tgz#fbeaf07d48fd878e0682bf7cbeeade728adb2b18" dependencies: - browserslist "^2.1.3" - caniuse-lite "^1.0.30000670" + browserslist "^2.1.5" + caniuse-lite "^1.0.30000697" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^6.0.1" + postcss "^6.0.6" postcss-value-parser "^3.2.3" aws-sign2@~0.6.0: @@ -640,9 +646,9 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-loader@^7.0.0, babel-loader@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.0.tgz#3fbf2581f085774bd9642dca9990e6d6c1491144" +babel-loader@^7.0.0, babel-loader@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -1080,9 +1086,9 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-preset-env@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" +babel-preset-env@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.5.2.tgz#cd4ae90a6e94b709f97374b33e5f8b983556adef" dependencies: babel-plugin-check-es2015-constants "^6.22.0" babel-plugin-syntax-trailing-function-commas "^6.22.0" @@ -1111,12 +1117,13 @@ babel-preset-env@1.4.0: babel-plugin-transform-es2015-unicode-regex "^6.22.0" babel-plugin-transform-exponentiation-operator "^6.22.0" babel-plugin-transform-regenerator "^6.22.0" - browserslist "^1.4.0" + browserslist "^2.1.2" invariant "^2.2.2" + semver "^5.3.0" -babel-preset-env@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.5.2.tgz#cd4ae90a6e94b709f97374b33e5f8b983556adef" +babel-preset-env@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4" dependencies: babel-plugin-check-es2015-constants "^6.22.0" babel-plugin-syntax-trailing-function-commas "^6.22.0" @@ -1191,8 +1198,8 @@ babel-preset-flow@^6.23.0: babel-plugin-transform-flow-strip-types "^6.22.0" babel-preset-react-app@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.0.0.tgz#f4505092f8bba0f0147c764dc72055fe46ac1416" + version "3.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.0.1.tgz#8b744cbe47fd57c868e6f913552ceae26ae31860" dependencies: babel-plugin-dynamic-import-node "1.0.2" babel-plugin-syntax-dynamic-import "6.18.0" @@ -1204,7 +1211,7 @@ babel-preset-react-app@^3.0.0: babel-plugin-transform-react-jsx-source "6.22.0" babel-plugin-transform-regenerator "6.24.1" babel-plugin-transform-runtime "6.23.0" - babel-preset-env "1.4.0" + babel-preset-env "1.5.2" babel-preset-react "6.24.1" babel-preset-react@6.24.1, babel-preset-react@^6.24.1: @@ -1404,8 +1411,8 @@ braces@^1.8.2: repeat-element "^1.1.2" brcast@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.0.tgz#9e627ab82209895664c1d6c1f45cd8c43422e3f6" + version "2.0.1" + resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.1.tgz#4311508f0634a6f5a2465b6cf2db27f06902aaca" brorand@^1.0.1: version "1.1.0" @@ -1466,14 +1473,14 @@ browserify-zlib@^0.1.4: dependencies: pako "~0.2.0" -browserslist@^1.3.6, browserslist@^1.4.0, browserslist@^1.5.2, browserslist@^1.7.6: +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: version "1.7.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" dependencies: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" -browserslist@^2.1.2, browserslist@^2.1.3: +browserslist@^2.1.2, browserslist@^2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.1.5.tgz#e882550df3d1cd6d481c1a3e0038f2baf13a4711" dependencies: @@ -1508,9 +1515,9 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" -bytes@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" +bytes@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" caller-path@^0.1.0: version "0.1.0" @@ -1559,12 +1566,12 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000696" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000696.tgz#e71f5c61e1f96c7a3af4e791ac5db55e11737604" + version "1.0.30000700" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000700.tgz#97cfc483865eea8577dc7a3674929b9abf553095" -caniuse-lite@^1.0.30000670, caniuse-lite@^1.0.30000684: - version "1.0.30000696" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000696.tgz#30f2695d2a01a0dfd779a26ab83f4d134b3da5cc" +caniuse-lite@^1.0.30000684, caniuse-lite@^1.0.30000697: + version "1.0.30000700" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000700.tgz#6084871ec75c6fa62327de97622514f95d9db26a" case-sensitive-paths-webpack-plugin@^2.0.0: version "2.1.1" @@ -1588,9 +1595,9 @@ chai-enzyme@^0.8.0: html "^1.0.0" react-element-to-jsx-string "^5.0.0" -chai@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.0.2.tgz#2f7327c4de6f385dd7787999e2ab02697a32b83b" +chai@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.0.tgz#331a0391b55c3af8740ae9c3b7458bc1c3805e6d" dependencies: assertion-error "^1.0.1" check-error "^1.0.1" @@ -1609,6 +1616,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + check-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -1650,10 +1665,11 @@ chokidar@^1.4.3, chokidar@^1.6.0: fsevents "^1.0.0" cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" dependencies: inherits "^2.0.1" + safe-buffer "^5.0.1" circular-json@^0.3.1: version "0.3.1" @@ -1713,8 +1729,8 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" coa@~1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.3.tgz#1b54a5e1dcf77c990455d4deea98c564416dc893" + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" dependencies: q "^1.1.2" @@ -1726,7 +1742,7 @@ collapse-white-space@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" -color-convert@^1.3.0: +color-convert@^1.0.0, color-convert@^1.3.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1775,10 +1791,8 @@ commander@2.9.0: graceful-readlink ">= 1.0.0" commander@^2.8.1, commander@^2.9.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.10.0.tgz#e1f5d3245de246d1a5ca04702fa1ad1bd7e405fe" - dependencies: - graceful-readlink ">= 1.0.0" + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" common-tags@^1.4.0: version "1.4.0" @@ -1790,11 +1804,11 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" -complex.js@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.1.tgz#ea90c7a05aeceaf3a376d2c0f6a78421727d6879" +complex.js@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373" -compressible@~2.0.8: +compressible@~2.0.10: version "2.0.10" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" dependencies: @@ -1810,15 +1824,16 @@ compression-webpack-plugin@^0.4.0: node-zopfli "^2.0.0" compression@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" + version "1.7.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" dependencies: accepts "~1.3.3" - bytes "2.3.0" - compressible "~2.0.8" - debug "~2.2.0" + bytes "2.5.0" + compressible "~2.0.10" + debug "2.6.8" on-headers "~1.0.1" - vary "~1.1.0" + safe-buffer "5.1.1" + vary "~1.1.1" concat-map@0.0.1: version "0.0.1" @@ -1984,8 +1999,8 @@ cryptiles@2.x.x: boom "2.x.x" crypto-browserify@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" dependencies: browserify-cipher "^1.0.0" browserify-sign "^4.0.0" @@ -2176,19 +2191,13 @@ debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" -decimal.js@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.1.1.tgz#1adcad7d70d7a91c426d756f1eb6566c3be6cbcf" +decimal.js@7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.2.3.tgz#6434c3b8a8c375780062fc633d0d2bbdb264cc78" deep-eql@^2.0.1: version "2.0.2" @@ -2281,10 +2290,14 @@ detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" -diff@3.2.0, diff@^3.1.0: +diff@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diff@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -2357,13 +2370,20 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domutils@1.5.1, domutils@^1.5.1: +domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + dot-prop@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1" @@ -2401,8 +2421,8 @@ ejs@^2.5.6: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.14: - version "1.3.14" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.14.tgz#64af0f9efd3c3c6acd57d71f83b49ca7ee9c4b43" + version "1.3.15" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369" element-class@^0.2.0: version "0.2.2" @@ -2421,8 +2441,8 @@ elliptic@^6.0.0: minimalistic-crypto-utils "^1.0.0" emoji-regex@^6.1.0: - version "6.4.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e" + version "6.4.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.3.tgz#6ac2ac58d4b78def5e39b33fcbf395688af3076c" emojione-picker@^2.2.1: version "2.2.1" @@ -2454,9 +2474,9 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -enhanced-resolve@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" +enhanced-resolve@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" @@ -2671,7 +2691,11 @@ esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^3.1.1, esprima@~3.1.0: +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -2806,12 +2830,12 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-text-webpack-plugin@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz#756ef4efa8155c3681833fbc34da53b941746d6c" +extract-text-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" dependencies: - async "^2.1.2" - loader-utils "^1.0.2" + async "^2.4.1" + loader-utils "^1.1.0" schema-utils "^0.3.0" webpack-sources "^1.0.1" @@ -2819,9 +2843,9 @@ extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" -fast-deep-equal@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" fast-levenshtein@~2.0.4: version "2.0.6" @@ -2999,9 +3023,9 @@ forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" -fraction.js@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.0.tgz#73974e2f8b51ef709536d624cc90782e2bb61274" +fraction.js@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.2.tgz#0eae896626f334b1bde763371347a83b5575d7f0" fresh@0.5.0: version "0.5.0" @@ -3050,12 +3074,12 @@ function-bind@^1.0.2, function-bind@^1.1.0: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" function.prototype.name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.0.tgz#5f523ca64e491a5f95aba80cc1e391080a14482e" + version "1.0.1" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.1.tgz#39aeab26bbf8ab669b7142965d50ea0965d93d7b" dependencies: define-properties "^1.1.2" function-bind "^1.1.0" - is-callable "^1.1.2" + is-callable "^1.1.3" fuse.js@^3.0.1: version "3.0.5" @@ -3133,8 +3157,8 @@ glamor@^2.20.25: prop-types "^15.5.8" glamorous@^3.22.1: - version "3.23.4" - resolved "https://registry.yarnpkg.com/glamorous/-/glamorous-3.23.4.tgz#a1e5f8045c332850105777dea4d3b21c5bdc4796" + version "3.23.5" + resolved "https://registry.yarnpkg.com/glamorous/-/glamorous-3.23.5.tgz#49f613a29f64cdee80948679c66dbcd4084e5fd5" dependencies: brcast "^2.0.0" fast-memoize "^2.2.7" @@ -3287,8 +3311,8 @@ hash-base@^2.0.0: inherits "^2.0.1" hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.2" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.2.tgz#bf5c887825cfe40b9efde7bf11bd2db26e6bf01b" + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" dependencies: inherits "^2.0.3" minimalistic-assert "^1.0.0" @@ -3436,10 +3460,14 @@ hyphenate-style-name@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" -iconv-lite@0.4.13, iconv-lite@~0.4.13: +iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" +iconv-lite@~0.4.13: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -3503,7 +3531,7 @@ ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inline-style-prefixer@^3.0.2: +inline-style-prefixer@^3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.6.tgz#b27fe309b4168a31eaf38c8e8c60ab9e7c11731f" dependencies: @@ -3560,12 +3588,24 @@ intl-messageformat@1.3.0, intl-messageformat@^1.3.0: dependencies: intl-messageformat-parser "1.2.0" +intl-messageformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.0.0.tgz#3d56982583425aee23b76c8b985fb9b0aae5be3c" + dependencies: + intl-messageformat-parser "1.2.0" + intl-relativeformat@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b" dependencies: intl-messageformat "1.3.0" +intl-relativeformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.0.0.tgz#d6ba9dc6c625819bc0abdb1d4e238138b7488f26" + dependencies: + intl-messageformat "^2.0.0" + intl@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" @@ -3612,7 +3652,7 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.2, is-callable@^1.1.3: +is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" @@ -3730,10 +3770,10 @@ is-plain-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" is-plain-object@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.3.tgz#c15bf3e4b66b62d72efaf2925848663ecbc619b6" + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" dependencies: - isobject "^3.0.0" + isobject "^3.0.1" is-posix-bracket@^0.1.0: version "0.1.1" @@ -3811,9 +3851,9 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" -isobject@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.0.tgz#39565217f3661789e8a0a0c080d5f7e6bc46e1a0" +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" isomorphic-fetch@^2.1.1: version "2.2.1" @@ -3826,20 +3866,24 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" +javascript-natural-sort@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" + js-base64@^2.1.8, js-base64@^2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.8.4: - version "3.8.4" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6" +js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce" dependencies: argparse "^1.0.7" - esprima "^3.1.1" + esprima "^4.0.0" js-yaml@~3.7.0: version "3.7.0" @@ -3852,9 +3896,9 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jsdom@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.0.0.tgz#1ee507cb2c0b16c875002476b1a8557d951353e5" +jsdom@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.1.0.tgz#6c48d7a48ffc5c300283c312904d15da8360509b" dependencies: abab "^1.0.3" acorn "^4.0.4" @@ -3865,7 +3909,7 @@ jsdom@^11.0.0: cssstyle ">= 0.2.37 < 0.3.0" escodegen "^1.6.1" html-encoding-sniffer "^1.0.1" - nwmatcher ">= 1.3.9 < 2.0.0" + nwmatcher "^1.4.1" parse5 "^3.0.2" pn "^1.0.0" request "^2.79.0" @@ -3875,7 +3919,7 @@ jsdom@^11.0.0: tough-cookie "^2.3.2" webidl-conversions "^4.0.0" whatwg-encoding "^1.0.1" - whatwg-url "^4.3.0" + whatwg-url "^6.1.0" xml-name-validator "^2.0.1" jsesc@^1.3.0: @@ -4015,15 +4059,6 @@ 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.16: - 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" @@ -4268,14 +4303,15 @@ math-expression-evaluator@^1.2.14: resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" mathjs@^3.11.5: - version "3.13.3" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.13.3.tgz#39135ea761f57c083da43638248e3f640727e290" + version "3.14.2" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.14.2.tgz#bb79b7dc878b7f586ce408ab067a9a42db2e7a2d" dependencies: - complex.js "2.0.1" - decimal.js "7.1.1" - fraction.js "4.0.0" + complex.js "2.0.4" + decimal.js "7.2.3" + fraction.js "4.0.2" + javascript-natural-sort "0.7.1" seed-random "2.2.0" - tiny-emitter "1.0.2" + tiny-emitter "2.0.0" typed-function "0.10.5" media-typer@0.3.0: @@ -4343,7 +4379,11 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0: +"mime-db@>= 1.27.0 < 2": + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + +mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" @@ -4353,10 +4393,14 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: dependencies: mime-db "~1.27.0" -mime@1.3.4, mime@1.3.x, mime@^1.3.4: +mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime@1.3.x, mime@^1.3.4: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -4426,10 +4470,6 @@ mocha@^3.4.1: mkdirp "0.5.1" supports-color "3.1.2" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" @@ -4648,7 +4688,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -"nwmatcher@>= 1.3.9 < 2.0.0": +nwmatcher@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.1.tgz#7ae9b07b0ea804db7e25f05cb5fe4097d4e4949f" @@ -5373,7 +5413,7 @@ postcss-sass@^0.1.0: mathjs "^3.11.5" postcss "^5.2.6" -postcss-scss@^1.0.0: +postcss-scss@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.2.tgz#ff45cf3354b879ee89a4eb68680f46ac9bb14f94" dependencies: @@ -5407,16 +5447,16 @@ postcss-simple-vars@^4.0.0: dependencies: postcss "^6.0.1" -postcss-smart-import@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/postcss-smart-import/-/postcss-smart-import-0.7.4.tgz#50cfb3d9a49b70a61f911451bc24d841f8dbf200" +postcss-smart-import@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/postcss-smart-import/-/postcss-smart-import-0.7.5.tgz#df9a9c6dd60d916e5e0670d1c57d03af5d3dcc31" dependencies: babel-runtime "^6.23.0" lodash "^4.17.4" object-assign "^4.1.1" - postcss "^6.0.1" + postcss "^6.0.6" postcss-sass "^0.1.0" - postcss-scss "^1.0.0" + postcss-scss "^1.0.2" postcss-value-parser "^3.3.0" promise-each "^2.2.0" read-cache "^1.0.0" @@ -5461,13 +5501,13 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.2, postcss@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.3.tgz#b7f565b3d956fbb8565ca7c1e239d0506e427d8b" +postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.2, postcss@^6.0.3, postcss@^6.0.6: + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.6.tgz#bba4d58e884fc78c840d1539e10eddaabb8f73bd" dependencies: - chalk "^1.1.3" + chalk "^2.0.1" source-map "^0.5.6" - supports-color "^4.0.0" + supports-color "^4.1.0" postgres-array@~1.0.0: version "1.0.2" @@ -5604,10 +5644,14 @@ q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" -qs@6.4.0, qs@^6.4.0, qs@~6.4.0: +qs@6.4.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@^6.4.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -5740,9 +5784,9 @@ react-immutable-pure-component@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-immutable-pure-component/-/react-immutable-pure-component-1.0.0.tgz#761d27b1497c5af64d2d2454e17b26ce7c9cda88" -react-inspector@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-2.0.0.tgz#c945932f2c2bf2fab7873c6e07d83881404b9313" +react-inspector@^2.0.0, react-inspector@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-2.1.1.tgz#2e68030d7ef0811a012f167258dd84232fd5ead1" dependencies: babel-runtime "^6.23.0" is-dom "^1.0.9" @@ -5871,11 +5915,11 @@ react-simple-dropdown@^3.0.0: prop-types "^15.5.8" react-split-pane@^0.1.63: - version "0.1.63" - resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.63.tgz#fadb3960cc659911dd05ffbc88acee4be9f53583" + version "0.1.64" + resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.64.tgz#725029b9fcb51059aa82f2b8622832473849f559" dependencies: - inline-style-prefixer "^3.0.2" - prop-types "^15.5.8" + inline-style-prefixer "^3.0.6" + prop-types "^15.5.10" react-style-proptype "^3.0.0" react-stubber@^1.0.0: @@ -5939,8 +5983,8 @@ react-toggle@^4.0.1: classnames "^2.2.5" react-virtualized@^9.7.4: - version "9.8.0" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.8.0.tgz#7c1fe9b723ce39a1c4916cabe1c4f1bda5dbc04b" + version "9.9.0" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.9.0.tgz#799a6f23819eeb82860d59b82fad33d1d420325e" dependencies: babel-runtime "^6.11.6" classnames "^2.2.3" @@ -5995,15 +6039,15 @@ read-pkg@^2.0.0: path-type "^2.0.0" readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9: - version "2.3.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.2.tgz#5a04df05e4f57fe3f0dc68fdd11dc5c97c7e6f4d" + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" isarray "~1.0.0" process-nextick-args "~1.0.6" - safe-buffer "~5.1.0" - string_decoder "~1.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" util-deprecate "~1.0.1" readdirp@^2.0.0: @@ -6322,7 +6366,7 @@ safe-buffer@5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -6503,9 +6547,9 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.3.5.tgz#9a2fc0ff8d526da716f30953aa2c65d518917f6c" +sinon@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.3.7.tgz#1451614a2eaab05bb4d876c1335cd40132ec5127" dependencies: diff "^3.1.0" formatio "1.2.0" @@ -6568,10 +6612,6 @@ source-list-map@^0.1.7, source-list-map@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" -source-list-map@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.2.tgz#9889019d1024cce55cdc069498337ef6186a11a1" - source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" @@ -6752,7 +6792,7 @@ string_decoder@^0.10.25: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.0: +string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: @@ -6822,9 +6862,9 @@ style-loader@^0.18.2: loader-utils "^1.0.2" schema-utils "^0.3.0" -substring-trie@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.0.tgz#5a7ecb83aefcca7b3720f7897cf69e97023be143" +substring-trie@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.1.tgz#1a5f07f774a91524eb067cb318dd4f3a3037bee0" sugarss@^1.0.0: version "1.0.0" @@ -6848,9 +6888,9 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.0.0.tgz#33a7c680aa512c9d03ef929cacbb974d203d2790" +supports-color@^4.0.0, supports-color@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.0.tgz#ad986dc7eb2315d009b4d77c8169c2231a684037" dependencies: has-flag "^2.0.0" @@ -6946,9 +6986,9 @@ timers-browserify@^2.0.2: dependencies: setimmediate "^1.0.4" -tiny-emitter@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.0.2.tgz#8e49470d3f55f89e247210368a6bb9fb51aa1601" +tiny-emitter@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.0.tgz#bad327adb1804b42a231afa741532bd884cd09ad" tiny-queue@^0.2.1: version "0.2.1" @@ -7035,7 +7075,7 @@ ua-parser-js@^0.7.9: version "0.7.13" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.13.tgz#cd9dd2f86493b3f44dbeeef3780fda74c5ee14be" -uglify-js@^2.8.27, uglify-js@^2.8.29: +uglify-js@^2.8.29: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" dependencies: @@ -7048,7 +7088,7 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uglifyjs-webpack-plugin@^0.4.4: +uglifyjs-webpack-plugin@^0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" dependencies: @@ -7163,7 +7203,7 @@ value-equal@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.2.1.tgz#c220a304361fce6994dbbedaa3c7e1a1b895871d" -vary@~1.1.0, vary@~1.1.1: +vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" @@ -7203,11 +7243,7 @@ wbuf@^1.1.0, wbuf@^1.7.2: dependencies: minimalistic-assert "^1.0.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - -webidl-conversions@^4.0.0: +webidl-conversions@^4.0.0, webidl-conversions@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" @@ -7263,17 +7299,17 @@ webpack-dev-server@^2.5.1: yargs "^6.0.0" webpack-hot-middleware@^2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.18.0.tgz#a16bb535b83a6ac94a78ac5ebce4f3059e8274d3" + version "2.18.2" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.18.2.tgz#84dee643f037c3d59c9de142548430371aa8d3b2" dependencies: ansi-html "0.0.7" html-entities "^1.2.0" querystring "^0.2.0" strip-ansi "^3.0.0" -webpack-manifest-plugin@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.1.0.tgz#6b6c718aade8a2537995784b46bd2e9836057caa" +webpack-manifest-plugin@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.1.2.tgz#e9d9967f4f739ee25380ca57de7f9417c5bea029" dependencies: fs-extra "^0.30.0" lodash ">=3.5 <5" @@ -7291,13 +7327,6 @@ webpack-sources@^0.1.0: source-list-map "~0.1.7" source-map "~0.5.3" -webpack-sources@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb" - dependencies: - source-list-map "^1.1.1" - source-map "~0.5.3" - webpack-sources@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" @@ -7305,42 +7334,16 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack@^2.5.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.6.1.tgz#2e0457f0abb1ac5df3ab106c69c672f236785f07" - dependencies: - acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" - ajv "^4.7.0" - ajv-keywords "^1.1.1" - async "^2.1.2" - enhanced-resolve "^3.0.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" - loader-runner "^2.3.0" - loader-utils "^0.2.16" - memory-fs "~0.4.1" - mkdirp "~0.5.0" - node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^3.1.0" - tapable "~0.2.5" - uglify-js "^2.8.27" - watchpack "^1.3.1" - webpack-sources "^0.2.3" - yargs "^6.0.0" - -webpack@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.0.0.tgz#ee9bcebf21247f7153cb410168cab45e3a59d4d7" +"webpack@^2.5.1 || ^3.0.0", webpack@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.2.0.tgz#8b0cae0e1a9fd76bfbf0eab61a8c2ada848c312f" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^5.1.5" ajv-keywords "^2.0.0" async "^2.1.2" - enhanced-resolve "^3.0.0" + enhanced-resolve "^3.3.0" escope "^3.6.0" interpret "^1.0.0" json-loader "^0.5.4" @@ -7353,7 +7356,7 @@ webpack@^3.0.0: source-map "^0.5.3" supports-color "^3.1.0" tapable "~0.2.5" - uglifyjs-webpack-plugin "^0.4.4" + uglifyjs-webpack-plugin "^0.4.6" watchpack "^1.3.1" webpack-sources "^1.0.1" yargs "^6.0.0" @@ -7384,12 +7387,13 @@ whatwg-fetch@>=0.10.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" -whatwg-url@^4.3.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" +whatwg-url@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.1.0.tgz#5fc8279b93d75483b9ced8b26239854847a18578" dependencies: + lodash.sortby "^4.7.0" tr46 "~0.0.3" - webidl-conversions "^3.0.0" + webidl-conversions "^4.0.1" whet.extend@~0.9.9: version "0.9.9" From 5abb3d815025675d1493da4f8c6e4dda4a6672e9 Mon Sep 17 00:00:00 2001 From: unarist Date: Thu, 13 Jul 2017 03:51:44 +0900 Subject: [PATCH 05/35] 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. --- .../mastodon/features/ui/components/modal_root.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 085299038ac..4240871a7cb 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) => } +
))} From e48d3bfd01c4bdeb36d39bbdf13b6873e3444179 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 13 Jul 2017 01:47:23 +0200 Subject: [PATCH 06/35] Fix #1010 - When spoiler text is set, enforce sensitivity too (#4176) --- app/models/status.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/status.rb b/app/models/status.rb index 791d96df1a4..edf80533631 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -124,6 +124,7 @@ class Status < ApplicationRecord before_validation :set_reblog before_validation :set_visibility before_validation :set_conversation + before_validation :set_sensitivity class << self def not_in_filtered_languages(account) @@ -250,6 +251,10 @@ class Status < ApplicationRecord self.visibility = (account.locked? ? :private : :public) if visibility.nil? end + def set_sensitivity + self.sensitive = sensitive || spoiler_text.present? + end + def set_conversation self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply From 880a5eb25cc62d2181b34a21985addee847cbb49 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 13 Jul 2017 03:12:25 +0200 Subject: [PATCH 07/35] Fix boolean columns sometimes having a null value (#4162) * Fix boolean columns sometimes having a null value * Fix wrong value being set instead of null --- app/models/domain_block.rb | 2 +- app/models/import.rb | 2 +- app/models/status.rb | 5 +++-- app/models/user.rb | 4 ++-- db/migrate/20170711225116_fix_null_booleans.rb | 17 +++++++++++++++++ db/schema.rb | 14 +++++++------- 6 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20170711225116_fix_null_booleans.rb diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 99dae9c1dd0..f26e8183ffc 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -8,7 +8,7 @@ # created_at :datetime not null # updated_at :datetime not null # severity :integer default("silence") -# reject_media :boolean +# reject_media :boolean default(FALSE), not null # class DomainBlock < ApplicationRecord diff --git a/app/models/import.rb b/app/models/import.rb index 8c6253d4983..815e0258983 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -6,7 +6,7 @@ # id :integer not null, primary key # account_id :integer not null # type :integer not null -# approved :boolean +# approved :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null # data_file_name :string diff --git a/app/models/status.rb b/app/models/status.rb index edf80533631..65db7579afc 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -12,12 +12,12 @@ # in_reply_to_id :integer # reblog_of_id :integer # url :string -# sensitive :boolean default(FALSE) +# sensitive :boolean default(FALSE), not null # visibility :integer default("public"), not null # in_reply_to_account_id :integer # application_id :integer # spoiler_text :text default(""), not null -# reply :boolean default(FALSE) +# reply :boolean default(FALSE), not null # favourites_count :integer default(0), not null # reblogs_count :integer default(0), not null # language :string @@ -249,6 +249,7 @@ class Status < ApplicationRecord def set_visibility self.visibility = (account.locked? ? :private : :public) if visibility.nil? + self.sensitive = false if sensitive.nil? end def set_sensitivity diff --git a/app/models/user.rb b/app/models/user.rb index c80115a08e3..86e57822587 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,7 +17,7 @@ # last_sign_in_at :datetime # current_sign_in_ip :inet # last_sign_in_ip :inet -# admin :boolean default(FALSE) +# admin :boolean default(FALSE), not null # confirmation_token :string # confirmed_at :datetime # confirmation_sent_at :datetime @@ -27,7 +27,7 @@ # encrypted_otp_secret_iv :string # encrypted_otp_secret_salt :string # consumed_timestep :integer -# otp_required_for_login :boolean +# otp_required_for_login :boolean default(FALSE), not null # last_emailed_at :datetime # otp_backup_codes :string is an Array # filtered_languages :string default([]), not null, is an Array diff --git a/db/migrate/20170711225116_fix_null_booleans.rb b/db/migrate/20170711225116_fix_null_booleans.rb new file mode 100644 index 00000000000..5b319471d70 --- /dev/null +++ b/db/migrate/20170711225116_fix_null_booleans.rb @@ -0,0 +1,17 @@ +class FixNullBooleans < ActiveRecord::Migration[5.1] + def change + change_column_default :domain_blocks, :reject_media, false + change_column_null :domain_blocks, :reject_media, false, false + + change_column_default :imports, :approved, false + change_column_null :imports, :approved, false, false + + change_column_null :statuses, :sensitive, false, false + change_column_null :statuses, :reply, false, false + + change_column_null :users, :admin, false, false + + change_column_default :users, :otp_required_for_login, false + change_column_null :users, :otp_required_for_login, false, false + end +end diff --git a/db/schema.rb b/db/schema.rb index 159704c6a18..0d5c73e2edf 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: 20170625140443) do +ActiveRecord::Schema.define(version: 20170711225116) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -89,7 +89,7 @@ ActiveRecord::Schema.define(version: 20170625140443) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "severity", default: 0 - t.boolean "reject_media" + t.boolean "reject_media", default: false, null: false t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true end @@ -121,7 +121,7 @@ ActiveRecord::Schema.define(version: 20170625140443) do create_table "imports", id: :serial, force: :cascade do |t| t.integer "account_id", null: false t.integer "type", null: false - t.boolean "approved" + t.boolean "approved", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "data_file_name" @@ -281,12 +281,12 @@ ActiveRecord::Schema.define(version: 20170625140443) do t.bigint "in_reply_to_id" t.bigint "reblog_of_id" t.string "url" - t.boolean "sensitive", default: false + t.boolean "sensitive", default: false, null: false t.integer "visibility", default: 0, null: false t.integer "in_reply_to_account_id" t.integer "application_id" t.text "spoiler_text", default: "", null: false - t.boolean "reply", default: false + t.boolean "reply", default: false, null: false t.integer "favourites_count", default: 0, null: false t.integer "reblogs_count", default: 0, null: false t.string "language" @@ -350,7 +350,7 @@ ActiveRecord::Schema.define(version: 20170625140443) do t.datetime "last_sign_in_at" t.inet "current_sign_in_ip" t.inet "last_sign_in_ip" - t.boolean "admin", default: false + t.boolean "admin", default: false, null: false t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" @@ -360,7 +360,7 @@ ActiveRecord::Schema.define(version: 20170625140443) do t.string "encrypted_otp_secret_iv" t.string "encrypted_otp_secret_salt" t.integer "consumed_timestep" - t.boolean "otp_required_for_login" + t.boolean "otp_required_for_login", default: false, null: false t.datetime "last_emailed_at" t.string "otp_backup_codes", array: true t.string "filtered_languages", default: [], null: false, array: true From c29c20ab3cda3d4b752c67868925c1fe99d0ac71 Mon Sep 17 00:00:00 2001 From: unarist Date: Thu, 13 Jul 2017 21:49:57 +0900 Subject: [PATCH 08/35] Add background color for spoiler input, like toot textarea (#4181) --- app/javascript/styles/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 8d0350eb600..45dd9f91425 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; From c2753fdfb471209fe7f2cdb8844e049207af8ba3 Mon Sep 17 00:00:00 2001 From: unarist Date: Fri, 14 Jul 2017 02:31:33 +0900 Subject: [PATCH 09/35] Make tag search case insensitive again (#4184) --- app/models/tag.rb | 2 +- ...20170713112503_make_tag_search_case_insensitive.rb | 11 +++++++++++ db/schema.rb | 4 ++-- spec/models/tag_spec.rb | 9 +++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20170713112503_make_tag_search_case_insensitive.rb diff --git a/app/models/tag.rb b/app/models/tag.rb index 08e3c1b03f0..a14e2cc989b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -23,7 +23,7 @@ class Tag < ApplicationRecord class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term) + '%' - Tag.where('name like ?', pattern).order(:name).limit(limit) + Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit) end end end diff --git a/db/migrate/20170713112503_make_tag_search_case_insensitive.rb b/db/migrate/20170713112503_make_tag_search_case_insensitive.rb new file mode 100644 index 00000000000..33ed6c00502 --- /dev/null +++ b/db/migrate/20170713112503_make_tag_search_case_insensitive.rb @@ -0,0 +1,11 @@ +class MakeTagSearchCaseInsensitive < ActiveRecord::Migration[5.1] + def up + remove_index :tags, name: :hashtag_search_index + execute 'CREATE INDEX hashtag_search_index ON tags (lower(name) text_pattern_ops);' + end + + def down + remove_index :tags, name: :hashtag_search_index + execute 'CREATE INDEX hashtag_search_index ON tags (name text_pattern_ops);' + end +end diff --git a/db/schema.rb b/db/schema.rb index 0d5c73e2edf..d6e572703d7 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: 20170711225116) do +ActiveRecord::Schema.define(version: 20170713112503) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -332,7 +332,7 @@ ActiveRecord::Schema.define(version: 20170711225116) do t.string "name", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index "name text_pattern_ops", name: "hashtag_search_index" + t.index "lower((name)::text) text_pattern_ops", name: "hashtag_search_index" t.index ["name"], name: "index_tags_on_name", unique: true end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 7c574eabe95..555474c4493 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -27,6 +27,15 @@ RSpec.describe Tag, type: :model do expect(results).to eq [tag] end + it 'finds tag records in case insensitive' do + tag = Fabricate(:tag, name: "MATCH") + _miss_tag = Fabricate(:tag, name: "miss") + + results = Tag.search_for("match") + + expect(results).to eq [tag] + end + it 'finds the exact matching tag as the first item' do similar_tag = Fabricate(:tag, name: "matchlater") tag = Fabricate(:tag, name: "match") From 0c7c188c459117770ac1f74f70a9e65ed2be606f Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Thu, 13 Jul 2017 22:15:32 +0200 Subject: [PATCH 10/35] 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 +++++ .../components/column_settings.js | 23 ++- .../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 +++++ .../mastodon/service_worker/entry.js | 1 + .../service_worker/web_push_notifications.js | 86 ++++++++ .../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 ++ ...713175513_create_web_push_subscriptions.rb | 12 ++ ...ush_subscription_to_session_activations.rb | 5 + db/schema.rb | 12 +- package.json | 1 + public/badge.png | Bin 0 -> 31156 bytes .../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 diff --git a/.env.production.sample b/.env.production.sample index 394cdedfef8..faefa24829e 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 38ebc934f28..868a843682f 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 b52685cba98..988b4d6b981 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 de0d6a10723..5599e1db160 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 00000000000..8425db7b45e --- /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 8a8b9ec76ba..1585bc81052 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 00000000000..55661d2b090 --- /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 26059489477..31cac5bc7a8 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 5108203587f..be1ff91d666 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 b139d4615c4..d4ead7881b5 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 d7ffa8ea6bf..d2c9d1c94ea 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 919345f165b..3aaf259c2af 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 00000000000..31a40d24612 --- /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 00000000000..364b670660b --- /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 00000000000..1708aa9f770 --- /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 00000000000..391d3bcec46 --- /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 45dd9f91425..02602afa4f7 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 a91d0d72a66..4966fbc216f 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 887e3e3bd4f..7eb16af8f47 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 86e57822587..a63b1da7f16 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 00000000000..4440706a69f --- /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 75fef28a859..9507aad4ab1 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 6751c941186..704d29a5747 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 407d385ea24..0ab61b634c5 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 71dcb54c63b..13ca9ea79e7 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 00000000000..0568a3e028c --- /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 406fa970b2d..4c60965c896 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 bde69eba15a..e68cb156dd9 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 00000000000..74e07377c69 --- /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 c9b5d9ab8f4..79efddfadb0 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 dc5aa716bd3..f9d69745faf 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 963fedcb42b..9171d02d4af 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 303fca81b27..4592db89e67 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 00000000000..4e5c2ba0012 --- /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 00000000000..d69cdfa5082 --- /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 d6e572703d7..b2c59a0f666 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 004c4d1f5bf..1aaa243c8bc 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 0000000000000000000000000000000000000000..fc1f42dca135ab028fbf3194a74329eb7c5103ee GIT binary patch literal 31156 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 literal 0 HcmV?d00001 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 00000000000..871176a07a0 --- /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 00000000000..72d11b77cc1 --- /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 00000000000..574da55ac26 --- /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 13c3f49518d..812a0721a5e 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" From a9a0c854e1df8fbc682eeb059fc68e8dbdbb5bde Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Thu, 13 Jul 2017 22:18:18 +0200 Subject: [PATCH 11/35] fix(components/media_modal): Style issues (#4187) --- .../components/extended_video_player.js | 4 +++- .../features/ui/components/media_modal.js | 20 +++++++++---------- app/javascript/styles/components.scss | 8 ++++++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js index 4c62fa7b3df..b38a4b8ff11 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 02602afa4f7..bcf7ba4e017 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; From a9067167bb368b1692bdd1ceea216b9215275ee2 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 00:49:01 +0200 Subject: [PATCH 12/35] 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 --- .../features/ui/components/columns_area.js | 20 +++++++++++++------ app/javascript/styles/components.scss | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index cbc185a7d60..ef9a1552236 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 bcf7ba4e017..0face646dde 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%; } From eeb5923e89c1b9040df37db8709e3450713c6019 Mon Sep 17 00:00:00 2001 From: unarist Date: Fri, 14 Jul 2017 08:59:34 +0900 Subject: [PATCH 13/35] 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(-) diff --git a/.postcssrc.yml b/.postcssrc.yml index 220fe0bb934..efffb39ba61 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 546b693b19b..3acc55abd68 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 bc546859532..df78891180a 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 1aaa243c8bc..5ad576dad61 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 812a0721a5e..56a9f7798c0 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" From 27b2355738482a2e470281136bfe902ad6c78db8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 03:51:17 +0200 Subject: [PATCH 14/35] Fix #3904 - Adjustable time period for mastodon:media:remove_remote via NUM_DAYS (#4191) --- lib/tasks/mastodon.rake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index b2b35285842..1b05ece33f8 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -85,9 +85,11 @@ namespace :mastodon do MediaAttachment.where(account: Account.silenced).find_each(&:destroy) end - desc 'Remove cached remote media attachments that are older than a week' + desc 'Remove cached remote media attachments that are older than NUM_DAYS. By default 7 (week)' task remove_remote: :environment do - MediaAttachment.where.not(remote_url: '').where('created_at < ?', 1.week.ago).find_each do |media| + time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago + + MediaAttachment.where.not(remote_url: '').where('created_at < ?', time_ago).find_each do |media| media.file.destroy media.type = :unknown media.save From a49be27145a858a51e1b9104b0ebc5f63a03b81b Mon Sep 17 00:00:00 2001 From: masarakki Date: Fri, 14 Jul 2017 18:02:49 +0900 Subject: [PATCH 15/35] add validation to tag name (#4194) --- app/models/tag.rb | 5 +++-- spec/models/tag_spec.rb | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index a14e2cc989b..0fa08e157c8 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -12,9 +12,10 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses - HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i + HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_][[:word:]_]*' + HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i - validates :name, presence: true, uniqueness: true + validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } def to_param name diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 555474c4493..f727fa1ddf8 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -1,6 +1,24 @@ require 'rails_helper' RSpec.describe Tag, type: :model do + describe 'validations' do + it 'invalid with #' do + expect(Tag.new(name: '#hello_world')).to_not be_valid + end + + it 'invalid with .' do + expect(Tag.new(name: '.abcdef123')).to_not be_valid + end + + it 'invalid with spaces' do + expect(Tag.new(name: 'hello world')).to_not be_valid + end + + it 'valid with aesthetic' do + expect(Tag.new(name: 'aesthetic')).to be_valid + end + end + describe 'HASHTAG_RE' do subject { Tag::HASHTAG_RE } From 87b96f8d339ff7998fc9a357f979f86cc38ad33e Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 14 Jul 2017 18:03:01 +0900 Subject: [PATCH 16/35] 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 +++ .../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(+) diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 6992e7e0f7e..7b890ce64b5 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 7a56e1446e1..0cf6bf3ac87 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 b2673915a38..1e44d6fa563 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 4b62403c3a1..f73011e73ae 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 7c1522299ac..aaa558c0ea2 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 778f33269f6..15afe230976 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 2648a68401c..4f9e26c258c 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 c42930380f1..64ba78716de 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 c9f1888b54c..306937cc23a 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 b836d2f5df6..1b17fb15540 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 eaa01638ca4..ea69532dddc 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 98c7ea02136..8b63bd26bbe 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 fdf5c11c01f..165e3088fcc 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 baf762c8dd2..71dcce50538 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 6f6d688e9fe..0c21877d83d 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 25e0adc8a83..788d09f34f2 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 4881b0f08eb..9176bfaafe6 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 f6207285228..a686cdc030b 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 5e1aaac85e2..0b47cc990c8 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 479d157f31e..cf6a8bd31c3 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 4bbf1493827..1f4082d7bc0 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 2c119ef413a..dc6dd5e32d1 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 c2288c4c07a..233d619957f 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 b199a39ce7e..cf2b911f2fb 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 b199a39ce7e..cf2b911f2fb 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 f9f48a48d98..942a13ede23 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 8a39beacb7e..e9e96c14fe7 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 203e4a09e1f..adfa79cd90e 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 c0f4a8dbbd3..435067281e0 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 998e1c8dac8..0f2c1fcec30 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 1079d54295b..c0b4cfce976 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 6240b887914..772cc691cdb 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 7cd97a0429b..37d82a20557 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: リダイレクト先が見つかりませんでした From 9008ab340712fafa6c26862138f5acbbc958bc9a Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 14 Jul 2017 18:08:56 +0900 Subject: [PATCH 17/35] 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 diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index d2c9d1c94ea..1f8ebb72145 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 00000000000..dd543910bb0 --- /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 00000000000..7b8ab5e5d85 --- /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 0b00da39d50..6e6ba34769f 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 f75f87c99bc..fd468bba06d 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' }/ From 489d1624777d86b710bd4f467857a7e7c3bd3128 Mon Sep 17 00:00:00 2001 From: Albert ARIBAUD Date: Fri, 14 Jul 2017 12:12:16 +0200 Subject: [PATCH 18/35] 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(-) diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index ea69532dddc..b6605295b4d 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 :", From 0fa9dd85273fed7f5412b9b1be21d8db7c5ed7bb Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 14 Jul 2017 19:13:43 +0900 Subject: [PATCH 19/35] Add Rake task for generate VAPID key (#4195) * Add Rake task for generate VAPID key * edit config/initializers/vapid.rb --- .env.production.sample | 2 +- config/initializers/vapid.rb | 2 +- lib/tasks/mastodon.rake | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.env.production.sample b/.env.production.sample index faefa24829e..eb1c5a48f50 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -36,7 +36,7 @@ OTP_SECRET= # 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;" +# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html VAPID_PRIVATE_KEY= diff --git a/config/initializers/vapid.rb b/config/initializers/vapid.rb index 74e07377c69..618f5a3fbbd 100644 --- a/config/initializers/vapid.rb +++ b/config/initializers/vapid.rb @@ -6,7 +6,7 @@ Rails.application.configure do # 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;" + # Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 1b05ece33f8..010139e91d3 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -184,6 +184,15 @@ namespace :mastodon do end end + namespace :webpush do + desc 'Generate VAPID key' + task generate_vapid_key: :environment do + vapid_key = Webpush.generate_key + puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}" + puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}" + end + end + namespace :maintenance do desc 'Update counter caches' task update_counter_caches: :environment do From 37430a340170a6b22e3ace44ad66ada756fa58a4 Mon Sep 17 00:00:00 2001 From: Albert ARIBAUD Date: Fri, 14 Jul 2017 13:55:40 +0200 Subject: [PATCH 20/35] fr: replace "silencer" with "masquer" throughout (#4197) * fr.json: replace "silencer" with "masquer" "Silencer" as a verb does not exist in French. A good and valid replacement is "masquer". * FR translation: use "masquer" for "silence" --- config/locales/fr.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c2efd0c851a..cba21765167 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -61,7 +61,7 @@ fr: media_attachments: Fichiers médias moderation: all: Tous - silenced: Muets + silenced: Masqués suspended: Suspendus title: Modération most_recent_activity: Dernière activité @@ -85,11 +85,11 @@ fr: created_reports: Signalements créés par ce compte report: signalement targeted_reports: Signalements créés visant ce compte - silence: Rendre muet + silence: Masquer statuses: Statuts subscribe: S’abonner title: Comptes - undo_silenced: Annuler le silence + undo_silenced: Démasquer undo_suspension: Annuler la suspension unsubscribe: Se désabonner username: Nom d’utilisateur⋅ice @@ -104,13 +104,13 @@ fr: hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. severity: desc_html: "Silence rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. Suspend supprimera tout le contenu des comptes concernés, les médias, et les données du profil." - silence: Muet + silence: Masqué suspend: Suspendre title: Nouveau blocage de domaine reject_media: Fichiers média rejetés reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions. severities: - silence: Rendre muet + silence: Masquer suspend: Suspendre severity: Séverité show: @@ -118,7 +118,7 @@ fr: one: Un compte affecté dans la base de données other: "%{count} comptes affectés dans la base de données" retroactive: - silence: Annuler le silence sur tous les comptes existants pour ce domaine + silence: Annuler le masquage sur tous les comptes existants pour ce domaine suspend: Annuler la suspension sur tous les comptes existants pour ce domaine title: Annuler le blocage de domaine pour %{domain} undo: Annuler @@ -145,7 +145,7 @@ fr: reported_account: Compte signalé reported_by: Signalé par resolved: Résolus - silence_account: Rendre le compte muet + silence_account: Masquer le compte status: Statut suspend_account: Suspendre le compte target: Cible From 999170d898fbe98e634f827580695c0615370f75 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Fri, 14 Jul 2017 14:41:13 +0200 Subject: [PATCH 21/35] =?UTF-8?q?i18n:=20fediwers=20=E2=80=BA=20fediwersum?= =?UTF-8?q?=20(pl)=20(#4198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- config/locales/pl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index f9d69745faf..9ee6c0540ea 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -300,7 +300,7 @@ pl: following: Lista śledzonych muting: Lista wyciszonych upload: Załaduj - landing_strip_html: "%{name} ma konto na %{link_to_root_path}. Możesz je śledzić i wejść z nim w interakcję jeśli masz konto gdziekolwiek w Fediwersie." + landing_strip_html: "%{name} ma konto na %{link_to_root_path}. Możesz je śledzić i wejść z nim w interakcję jeśli masz konto gdziekolwiek w Fediwersum." landing_strip_signup_html: Jeśli jeszcze go nie masz, możesz stworzyć konto. media_attachments: validations: From c42092ba7ad1cbf78ca8463b6525ec2b7efbdd92 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 16:41:02 +0200 Subject: [PATCH 22/35] Add option to opt out of search engines on public profile/status pages (#4199) --- app/controllers/settings/preferences_controller.rb | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/views/accounts/show.html.haml | 3 +++ app/views/follower_accounts/index.html.haml | 3 +++ app/views/following_accounts/index.html.haml | 3 +++ app/views/layouts/embedded.html.haml | 2 ++ app/views/settings/preferences/show.html.haml | 3 +++ app/views/stream_entries/show.html.haml | 3 +++ config/locales/simple_form.en.yml | 2 ++ config/settings.yml | 1 + 11 files changed, 30 insertions(+) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index cac5b0ba8f6..a3f5a008b8f 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController :setting_delete_modal, :setting_auto_play_gif, :setting_system_font_ui, + :setting_noindex, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index e0e92b19d06..c5da180291f 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -23,6 +23,7 @@ class UserSettingsDecorator user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference user.settings['system_font_ui'] = system_font_ui_preference + user.settings['noindex'] = noindex_preference end def merged_notification_emails @@ -57,6 +58,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_auto_play_gif' end + def noindex_preference + boolean_cast_setting 'setting_noindex' + end + def boolean_cast_setting(key) settings[key] == '1' end diff --git a/app/models/user.rb b/app/models/user.rb index a63b1da7f16..becf0018fbe 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,6 +99,10 @@ class User < ApplicationRecord settings.system_font_ui end + def setting_noindex + settings.noindex + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index a1904910358..7ed634e5d28 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -2,6 +2,9 @@ = display_name(@account) - content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index 89c7f3a2947..738b3163817 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -2,6 +2,9 @@ = t('accounts.people_who_follow', name: display_name(@account)) - content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) = render 'accounts/header', account: @account diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 6f0de7590ba..9637c689f99 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -2,6 +2,9 @@ = t('accounts.people_followed_by', name: display_name(@account)) - content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) = render 'accounts/header', account: @account diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 4826f32f75f..46dab2d0fa2 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -2,6 +2,8 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ + %meta{ name: 'robots', content: 'noindex' }/ + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 56a261ab65d..3b5d90942a8 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -40,6 +40,9 @@ = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_noindex, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index d01e82af8ff..80ea30eb169 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -1,4 +1,7 @@ - content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index e3f784a1a78..fbaf0ff6892 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -12,6 +12,7 @@ en: note: one: 1 character left other: %{count} characters left + setting_noindex: Affects your public profile and status pages imports: data: CSV file exported from another Mastodon instance sessions: @@ -41,6 +42,7 @@ en: setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot setting_system_font_ui: Use system's default font + setting_noindex: Opt-out of search engine indexing severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index 579ba2929ad..d239fb6a65b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -21,6 +21,7 @@ defaults: &defaults auto_play_gif: false delete_modal: true system_font_ui: false + noindex: false notification_emails: follow: false reblog: false From e2685ccc81f04e1a63a97af80686bf85027418a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 19:47:53 +0200 Subject: [PATCH 23/35] 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 ++----------- .../compose/components/compose_form.js | 3 +- .../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 diff --git a/Gemfile b/Gemfile index 988b4d6b981..531d01ae059 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 5599e1db160..83202189d0f 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 00000000000..c1595851fdb --- /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 647a52b9325..9f05a53e965 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 7043d5f3a35..ed2180cd1de 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 f7eeedc69f7..f0755294794 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 83c66a5d594..acc584f20cf 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 a92b5aa236b..ea3b78b67a1 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 0face646dde..0420a2bedc6 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 2b54cee5fc9..7243cb1a58f 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 65db7579afc..24eaf7071f7 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 2e6fbb5c382..951a38e1957 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 00000000000..1eedfb71946 --- /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 3cd397397b1..00000000000 --- 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 3e8b25af935..e165b4519fc 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('

'); }); - }); From 8d224ad23bca8d4923e230e5495ee60197e44c3b Mon Sep 17 00:00:00 2001 From: unarist Date: Sat, 15 Jul 2017 02:57:49 +0900 Subject: [PATCH 24/35] Follow renaming of microformats2 gem (#4203) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- spec/views/stream_entries/show.html.haml_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 531d01ae059..5d5ddfae109 100644 --- a/Gemfile +++ b/Gemfile @@ -79,7 +79,7 @@ group :test do gem 'capybara', '~> 2.14' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.7' - gem 'microformats2', '~> 3.0' + gem 'microformats', '~> 4.0' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.14', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 83202189d0f..daef3e1ad86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -242,7 +242,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) method_source (0.8.2) - microformats2 (3.1.0) + microformats (4.0.7) json nokogiri mime-types (3.1) @@ -533,7 +533,7 @@ DEPENDENCIES letter_opener_web (~> 1.3) link_header (~> 0.0) lograge (~> 0.5) - microformats2 (~> 3.0) + microformats (~> 4.0) mime-types (~> 3.1) nokogiri (~> 1.7) oj (~> 3.0) diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb index cc380e6eafa..6cc3b117aaa 100644 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ b/spec/views/stream_entries/show.html.haml_spec.rb @@ -27,7 +27,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d render - mf2 = Microformats2.parse(rendered) + mf2 = Microformats.parse(rendered) expect(mf2.entry.name.to_s).to eq status.text expect(mf2.entry.url.to_s).not_to be_empty @@ -53,7 +53,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d render - mf2 = Microformats2.parse(rendered) + mf2 = Microformats.parse(rendered) expect(mf2.entry.name.to_s).to eq reply.text expect(mf2.entry.url.to_s).not_to be_empty From c1f201c49a007e5c0740c00651e549a7b0416b05 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 20:30:12 +0200 Subject: [PATCH 25/35] =?UTF-8?q?=F0=9F=8E=84=F0=9F=94=A8=20Force=20tree?= =?UTF-8?q?=20shake=20emojione=20(#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 diff --git a/.babelrc b/.babelrc index 19968964eee..de922f3899a 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 ed2180cd1de..1de41f57225 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 00000000000..c75e10a98bf --- /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 ef97fb12762..82b20810a2d 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 5ad576dad61..20224796f6f 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 e165b4519fc..2874bb56d30 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 56a9f7798c0..defd8599f8b 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" From 1618b68bfa740ed655ac45d7d5f4f46fed6c8c62 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 20:41:49 +0200 Subject: [PATCH 26/35] HTTP signatures (#4146) * Add Request class with HTTP signature generator Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06 * Add HTTP signature verification concern * Add test for SignatureVerification concern * Add basic test for Request class * Make PuSH subscribe/unsubscribe requests use new Request class Accidentally fix lease_seconds not being set and sent properly, and change the new minimum subscription duration to 1 day * Make all PuSH workers use new Request class * Make Salmon sender use new Request class * Make FetchLinkService use new Request class * Make FetchAtomService use the new Request class * Make Remotable use the new Request class * Make ResolveRemoteAccountService use the new Request class * Add more tests * Allow +-30 seconds window for signed request to remain valid * Disable time window validation for signed requests, restore 7 days as PuSH subscription duration (which was previous default due to a bug) --- app/controllers/accounts_controller.rb | 1 + .../api/subscriptions_controller.rb | 2 +- .../concerns/signature_verification.rb | 87 +++++++++++++++++++ app/controllers/stream_entries_controller.rb | 1 + app/helpers/http_helper.rb | 17 ---- app/lib/provider_discovery.rb | 4 +- app/lib/request.rb | 70 +++++++++++++++ app/models/account.rb | 2 +- app/models/concerns/remotable.rb | 3 +- app/models/subscription.rb | 4 +- app/services/fetch_atom_service.rb | 8 +- app/services/fetch_link_card_service.rb | 6 +- .../resolve_remote_account_service.rb | 3 +- app/services/send_interaction_service.rb | 14 ++- app/services/subscribe_service.rb | 48 +++++++--- app/services/unsubscribe_service.rb | 31 +++++-- .../pubsubhubbub/confirmation_worker.rb | 12 ++- app/workers/pubsubhubbub/delivery_worker.rb | 11 ++- .../concerns/signature_verification_spec.rb | 74 ++++++++++++++++ spec/helpers/http_helper_spec.rb | 13 --- spec/lib/request_spec.rb | 54 ++++++++++++ .../pubsubhubbub/confirmation_worker_spec.rb | 2 +- .../pubsubhubbub/delivery_worker_spec.rb | 2 +- 23 files changed, 379 insertions(+), 90 deletions(-) create mode 100644 app/controllers/concerns/signature_verification.rb delete mode 100644 app/helpers/http_helper.rb create mode 100644 app/lib/request.rb create mode 100644 spec/controllers/concerns/signature_verification_spec.rb delete mode 100644 spec/helpers/http_helper_spec.rb create mode 100644 spec/lib/request_spec.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 11402ab79e4..69b520df1e9 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -2,6 +2,7 @@ class AccountsController < ApplicationController include AccountControllerConcern + include SignatureVerification def show respond_to do |format| diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb index d3ea986761d..89007f3d6ef 100644 --- a/app/controllers/api/subscriptions_controller.rb +++ b/app/controllers/api/subscriptions_controller.rb @@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController end def lease_seconds_or_default - (params['hub.lease_seconds'] || 86_400).to_i.seconds + (params['hub.lease_seconds'] || 1.day).to_i.seconds end def set_account diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb new file mode 100644 index 00000000000..abe845d9374 --- /dev/null +++ b/app/controllers/concerns/signature_verification.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Implemented according to HTTP signatures (Draft 6) +# +module SignatureVerification + extend ActiveSupport::Concern + + def signed_request? + request.headers['Signature'].present? + end + + def signed_request_account + return @signed_request_account if defined?(@signed_request_account) + + unless signed_request? + @signed_request_account = nil + return + end + + raw_signature = request.headers['Signature'] + signature_params = {} + + raw_signature.split(',').each do |part| + parsed_parts = part.match(/([a-z]+)="([^"]+)"/i) + next if parsed_parts.nil? || parsed_parts.size != 3 + signature_params[parsed_parts[1]] = parsed_parts[2] + end + + if incompatible_signature?(signature_params) + @signed_request_account = nil + return + end + + account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, '')) + + if account.nil? + @signed_request_account = nil + return + end + + signature = Base64.decode64(signature_params['signature']) + compare_signed_string = build_signed_string(signature_params['headers']) + + if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string) + @signed_request_account = account + @signed_request_account + else + @signed_request_account = nil + end + end + + private + + def build_signed_string(signed_headers) + signed_headers = 'date' if signed_headers.blank? + + signed_headers.split(' ').map do |signed_header| + if signed_header == Request::REQUEST_TARGET + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + else + "#{signed_header}: #{request.headers[to_header_name(signed_header)]}" + end + end.join("\n") + end + + def matches_time_window? + begin + time_sent = DateTime.httpdate(request.headers['Date']) + rescue ArgumentError + return false + end + + (Time.now.utc - time_sent).abs <= 30 + end + + def to_header_name(name) + name.split(/-/).map(&:capitalize).join('-') + end + + def incompatible_signature?(signature_params) + signature_params['keyId'].blank? || + signature_params['signature'].blank? || + signature_params['algorithm'].blank? || + signature_params['algorithm'] != 'rsa-sha256' || + !signature_params['keyId'].start_with?('acct:') + end +end diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 314d5961909..54a43523845 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -2,6 +2,7 @@ class StreamEntriesController < ApplicationController include Authorization + include SignatureVerification layout 'public' diff --git a/app/helpers/http_helper.rb b/app/helpers/http_helper.rb deleted file mode 100644 index e39a52da01f..00000000000 --- a/app/helpers/http_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module HttpHelper - def http_client(options = {}) - timeout = { write: 10, connect: 10, read: 10 }.merge(options) - - HTTP.headers(user_agent: user_agent) - .timeout(:per_operation, timeout) - .follow - end - - private - - def user_agent - @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +http://#{Rails.configuration.x.local_domain}/)" - end -end diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb index 6d48cae2f07..5e02e68066f 100644 --- a/app/lib/provider_discovery.rb +++ b/app/lib/provider_discovery.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class ProviderDiscovery < OEmbed::ProviderDiscovery - extend HttpHelper - class << self def discover_provider(url, options = {}) - res = http_client.get(url) + res = Request.new(:get, url).perform format = options[:format] raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html' diff --git a/app/lib/request.rb b/app/lib/request.rb new file mode 100644 index 00000000000..e73c5ac20b8 --- /dev/null +++ b/app/lib/request.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class Request + REQUEST_TARGET = '(request-target)' + + include RoutingHelper + + def initialize(verb, url, options = {}) + @verb = verb + @url = Addressable::URI.parse(url).normalize + @options = options + @headers = {} + + set_common_headers! + end + + def on_behalf_of(account) + raise ArgumentError unless account.local? + @account = account + end + + def add_headers(new_headers) + @headers.merge!(new_headers) + end + + def perform + http_client.headers(headers).public_send(@verb, @url.to_s, @options) + end + + def headers + (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET) + end + + private + + def set_common_headers! + @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" + @headers['User-Agent'] = user_agent + @headers['Host'] = @url.host + @headers['Date'] = Time.now.utc.httpdate + end + + def signature + key_id = @account.to_webfinger_s + algorithm = 'rsa-sha256' + signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string)) + + "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers}\",signature=\"#{signature}\"" + end + + def signed_string + @headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + end + + def signed_headers + @headers.keys.join(' ').downcase + end + + def user_agent + @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" + end + + def timeout + { write: 10, connect: 10, read: 10 } + end + + def http_client + HTTP.timeout(:per_operation, timeout).follow + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 7243cb1a58f..58b0a1086ba 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -130,7 +130,7 @@ class Account < ApplicationRecord end def subscription(webhook_url) - OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 86_400 * 30, webhook: webhook_url, hub: hub_url) + OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 30.days.seconds, webhook: webhook_url, hub: hub_url) end def save_with_optional_media! diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index b4f16964928..1bd87a642ef 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Remotable - include HttpHelper extend ActiveSupport::Concern included do @@ -20,7 +19,7 @@ module Remotable return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url begin - response = http_client.get(url) + response = Request.new(:get, url).perform return if response.code != 200 diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 35a228df072..d9d5024a964 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -16,8 +16,8 @@ # class Subscription < ApplicationRecord - MIN_EXPIRATION = 7.days.seconds.to_i - MAX_EXPIRATION = 30.days.seconds.to_i + MIN_EXPIRATION = 1.day.to_i + MAX_EXPIRATION = 30.days.to_i belongs_to :account, required: true diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index d430b22e93f..3ac441e3e81 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -1,16 +1,14 @@ # frozen_string_literal: true class FetchAtomService < BaseService - include HttpHelper - def call(url) return if url.blank? - response = http_client.head(url) + response = Request.new(:head, url).perform Rails.logger.debug "Remote status HEAD request returned code #{response.code}" - response = http_client.get(url) if response.code == 405 + response = Request.new(:get, url).perform if response.code == 405 Rails.logger.debug "Remote status GET request returned code #{response.code}" @@ -49,6 +47,6 @@ class FetchAtomService < BaseService end def fetch(url) - http_client.get(url).to_s + Request.new(:get, url).perform.to_s end end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 6ef3abb6671..20c85e0ea32 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchLinkCardService < BaseService - include HttpHelper - URL_PATTERN = %r{https?://\S+} def call(status) @@ -13,7 +11,7 @@ class FetchLinkCardService < BaseService url = url.to_s card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) - res = http_client.head(url) + res = Request.new(:head, url).perform return if res.code != 200 || res.mime_type != 'text/html' @@ -80,7 +78,7 @@ class FetchLinkCardService < BaseService end def attempt_opengraph(card, url) - response = http_client.get(url) + response = Request.new(:get, url).perform return if response.code != 200 || response.mime_type != 'text/html' diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_remote_account_service.rb index 362d0df98b1..d2dfda82472 100644 --- a/app/services/resolve_remote_account_service.rb +++ b/app/services/resolve_remote_account_service.rb @@ -2,7 +2,6 @@ class ResolveRemoteAccountService < BaseService include OStatus2::MagicKey - include HttpHelper DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' @@ -79,7 +78,7 @@ class ResolveRemoteAccountService < BaseService end def get_feed(url) - response = http_client(write: 20, connect: 20, read: 50).get(Addressable::URI.parse(url).normalize) + response = Request.new(:get, url).perform raise Goldfinger::Error, "Feed attempt failed for #{url}: HTTP #{response.code}" unless response.code == 200 [response.to_s, Nokogiri::XML(response)] end diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb index 34c8f9e34ab..ef38a748b21 100644 --- a/app/services/send_interaction_service.rb +++ b/app/services/send_interaction_service.rb @@ -12,13 +12,23 @@ class SendInteractionService < BaseService return if block_notification? - envelope = salmon.pack(@xml, @source_account.keypair) - delivery = salmon.post(@target_account.salmon_url, envelope) + delivery = build_request.perform + raise "Delivery failed for #{target_account.salmon_url}: HTTP #{delivery.code}" unless delivery.code > 199 && delivery.code < 300 end private + def build_request + request = Request.new(:post, @target_account.salmon_url, body: envelope) + request.add_headers('Content-Type' => 'application/magic-envelope+xml') + request + end + + def envelope + salmon.pack(@xml, @source_account.keypair) + end + def block_notification? DomainBlock.blocked?(@target_account.domain) end diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb index 1e7984a7f75..f58067038bb 100644 --- a/app/services/subscribe_service.rb +++ b/app/services/subscribe_service.rb @@ -2,34 +2,54 @@ class SubscribeService < BaseService def call(account) - account.secret = SecureRandom.hex + @account = account + @account.secret = SecureRandom.hex + @response = build_request.perform - subscription = account.subscription(api_subscription_url(account.id)) - response = subscription.subscribe - - if response_failed_permanently?(response) + if response_failed_permanently? # We're not allowed to subscribe. Fail and move on. - account.secret = '' - account.save! - elsif response_successful?(response) + @account.secret = '' + @account.save! + elsif response_successful? # The subscription will be confirmed asynchronously. - account.save! + @account.save! else # The response was either a 429 rate limit, or a 5xx error. # We need to retry at a later time. Fail loudly! - raise "Subscription attempt failed for #{account.acct} (#{account.hub_url}): HTTP #{response.code}" + raise "Subscription attempt failed for #{@account.acct} (#{@account.hub_url}): HTTP #{@response.code}" end end private + def build_request + request = Request.new(:post, @account.hub_url, form: subscription_params) + request.on_behalf_of(some_local_account) if some_local_account + request + end + + def subscription_params + { + 'hub.topic': @account.remote_url, + 'hub.mode': 'subscribe', + 'hub.callback': api_subscription_url(@account.id), + 'hub.verify': 'async', + 'hub.secret': @account.secret, + 'hub.lease_seconds': 7.days.seconds, + } + end + + def some_local_account + @some_local_account ||= Account.local.first + end + # Any response in the 3xx or 4xx range, except for 429 (rate limit) - def response_failed_permanently?(response) - (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests? + def response_failed_permanently? + (@response.status.redirect? || @response.status.client_error?) && !@response.status.too_many_requests? end # Any response in the 2xx range - def response_successful?(response) - response.status.success? + def response_successful? + @response.status.success? end end diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb index 6db8dbdc49a..c2f022d7dfe 100644 --- a/app/services/unsubscribe_service.rb +++ b/app/services/unsubscribe_service.rb @@ -2,17 +2,30 @@ class UnsubscribeService < BaseService def call(account) - subscription = account.subscription(api_subscription_url(account.id)) - response = subscription.unsubscribe + @account = account + @response = build_request.perform - unless response.status.success? - Rails.logger.debug "PuSH unsubscribe for #{account.acct} failed: #{response.status}" - end + Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success? - account.secret = '' - account.subscription_expires_at = nil - account.save! + @account.secret = '' + @account.subscription_expires_at = nil + @account.save! rescue HTTP::Error, OpenSSL::SSL::SSLError - Rails.logger.debug "PuSH subscription request for #{account.acct} could not be made due to HTTP or SSL error" + Rails.logger.debug "PuSH subscription request for #{@account.acct} could not be made due to HTTP or SSL error" + end + + private + + def build_request + Request.new(:post, @account.hub_url, form: subscription_params) + end + + def subscription_params + { + 'hub.topic': @account.remote_url, + 'hub.mode': 'unsubscribe', + 'hub.callback': api_subscription_url(@account.id), + 'hub.verify': 'async', + } end end diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb index 9186c5d7d7c..e1ccfb99c6f 100644 --- a/app/workers/pubsubhubbub/confirmation_worker.rb +++ b/app/workers/pubsubhubbub/confirmation_worker.rb @@ -60,9 +60,7 @@ class Pubsubhubbub::ConfirmationWorker end def callback_get_with_params - HTTP.headers(user_agent: 'Mastodon/PubSubHubbub') - .timeout(:per_operation, write: 20, connect: 20, read: 50) - .get(subscription.callback_url, params: callback_params) + Request.new(:get, subscription.callback_url, params: callback_params).perform end def callback_response_body @@ -71,10 +69,10 @@ class Pubsubhubbub::ConfirmationWorker def callback_params { - 'hub.topic' => account_url(subscription.account, format: :atom), - 'hub.mode' => mode, - 'hub.challenge' => challenge, - 'hub.lease_seconds' => subscription.lease_seconds, + 'hub.topic': account_url(subscription.account, format: :atom), + 'hub.mode': mode, + 'hub.challenge': challenge, + 'hub.lease_seconds': subscription.lease_seconds, } end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 981838f3314..05d160cf7c7 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -33,9 +33,9 @@ class Pubsubhubbub::DeliveryWorker end def callback_post_payload - HTTP.timeout(:per_operation, write: 50, connect: 20, read: 50) - .headers(headers) - .post(subscription.callback_url, body: payload) + request = Request.new(:post, subscription.callback_url, body: payload) + request.add_headers(headers) + request.perform end def blocked_domain? @@ -48,13 +48,12 @@ class Pubsubhubbub::DeliveryWorker def headers { - 'User-Agent' => 'Mastodon/PubSubHubbub', 'Content-Type' => 'application/atom+xml', - 'Link' => link_headers, + 'Link' => link_header, }.merge(signature_headers.to_h) end - def link_headers + def link_header LinkHeader.new([hub_link_header, self_link_header]).to_s end diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb new file mode 100644 index 00000000000..b371795ab06 --- /dev/null +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ApplicationController, type: :controller do + controller do + include SignatureVerification + + def success + head 200 + end + + def alternative_success + head 200 + end + end + + before do + routes.draw { get 'success' => 'anonymous#success' } + end + + context 'without signature header' do + before do + get :success + end + + describe '#signed_request?' do + it 'returns false' do + expect(controller.signed_request?).to be false + end + end + + describe '#signed_request_account' do + it 'returns nil' do + expect(controller.signed_request_account).to be_nil + end + end + end + + context 'with signature header' do + let!(:author) { Fabricate(:account) } + + before do + get :success + + fake_request = Request.new(:get, request.url) + fake_request.on_behalf_of(author) + + request.headers.merge!(fake_request.headers) + end + + describe '#signed_request?' do + it 'returns true' do + expect(controller.signed_request?).to be true + end + end + + describe '#signed_request_account' do + it 'returns an account' do + expect(controller.signed_request_account).to eq author + end + + it 'returns nil when path does not match' do + request.path = '/alternative-path' + expect(controller.signed_request_account).to be_nil + end + + it 'returns nil when method does not match' do + post :success + expect(controller.signed_request_account).to be_nil + end + end + end +end diff --git a/spec/helpers/http_helper_spec.rb b/spec/helpers/http_helper_spec.rb deleted file mode 100644 index b8e31b8e6f3..00000000000 --- a/spec/helpers/http_helper_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe HttpHelper do - describe 'http_client' do - it 'returns HTTP::Client with default options' do - options = helper.http_client.default_options - expect(options.headers['User-Agent']).to match /.+ \(Mastodon\/.+;\ \+http:\/\/cb6e6126\.ngrok\.io\/\)/ - expect(options.timeout_options).to eq read_timeout: 10, write_timeout: 10, connect_timeout: 10 - end - end -end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb new file mode 100644 index 00000000000..782f14b1809 --- /dev/null +++ b/spec/lib/request_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Request do + subject { Request.new(:get, 'http://example.com') } + + describe '#headers' do + it 'returns user agent' do + expect(subject.headers['User-Agent']).to be_present + end + + it 'returns the date header' do + expect(subject.headers['Date']).to be_present + end + + it 'returns the host header' do + expect(subject.headers['Host']).to be_present + end + + it 'does not return virtual request-target header' do + expect(subject.headers['(request-target)']).to be_nil + end + end + + describe '#on_behalf_of' do + it 'when used, adds signature header' do + subject.on_behalf_of(Fabricate(:account)) + expect(subject.headers['Signature']).to be_present + end + end + + describe '#add_headers' do + it 'adds headers to the request' do + subject.add_headers('Test' => 'Foo') + expect(subject.headers['Test']).to eq 'Foo' + end + end + + describe '#perform' do + before do + stub_request(:get, 'http://example.com') + subject.perform + end + + it 'executes a HTTP request' do + expect(a_request(:get, 'http://example.com')).to have_been_made.once + end + + it 'sets headers' do + expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made + end + end +end diff --git a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb b/spec/workers/pubsubhubbub/confirmation_worker_spec.rb index 1199d580190..8f66b4520c0 100644 --- a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb +++ b/spec/workers/pubsubhubbub/confirmation_worker_spec.rb @@ -83,6 +83,6 @@ describe Pubsubhubbub::ConfirmationWorker do end def http_headers - { 'Connection' => 'close', 'Host' => 'example.com', 'User-Agent' => 'Mastodon/PubSubHubbub' } + { 'Connection' => 'close', 'Host' => 'example.com', 'User-Agent' => 'http.rb/2.2.2 (Mastodon/1.4.7; +https://cb6e6126.ngrok.io/)' } end end diff --git a/spec/workers/pubsubhubbub/delivery_worker_spec.rb b/spec/workers/pubsubhubbub/delivery_worker_spec.rb index 081dfa41c0e..a83245786bf 100644 --- a/spec/workers/pubsubhubbub/delivery_worker_spec.rb +++ b/spec/workers/pubsubhubbub/delivery_worker_spec.rb @@ -59,7 +59,7 @@ describe Pubsubhubbub::DeliveryWorker do 'Content-Type' => 'application/atom+xml', 'Host' => 'example.com', 'Link' => "; rel=\"hub\", ; rel=\"self\"", - 'User-Agent' => 'Mastodon/PubSubHubbub', + 'User-Agent' => 'http.rb/2.2.2 (Mastodon/1.4.7; +https://cb6e6126.ngrok.io/)', }.tap do |basic| known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload) basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret? From 1896a154f5f5edd4f94c58e5c69dd70ebc728e3d Mon Sep 17 00:00:00 2001 From: unarist Date: Sat, 15 Jul 2017 03:44:53 +0900 Subject: [PATCH 27/35] Fix response of unreblog/unfavourite APIs (#4204) Both APIs process asynchronously, so reblogged/favourited fields in the response should be set to `false` manually. --- app/controllers/api/v1/statuses/favourites_controller.rb | 2 +- app/controllers/api/v1/statuses/reblogs_controller.rb | 2 +- app/presenters/status_relationships_presenter.rb | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 4c4b0c1604a..35f8a48cd6c 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController UnfavouriteWorker.perform_async(current_user.account_id, @status.id) - render json: @status, serializer: REST::StatusSerializer + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, favourites_map: @favourites_map) end private diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index f7f4b5a5c6d..634af474f9b 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController authorize status_for_destroy, :unreblog? RemovalWorker.perform_async(status_for_destroy.id) - render json: @status, serializer: REST::StatusSerializer + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map) end private diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index caf00791a3f..03294015f37 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -3,7 +3,7 @@ class StatusRelationshipsPresenter attr_reader :reblogs_map, :favourites_map, :mutes_map - def initialize(statuses, current_account_id = nil) + def initialize(statuses, current_account_id = nil, reblogs_map: {}, favourites_map: {}, mutes_map: {}) if current_account_id.nil? @reblogs_map = {} @favourites_map = {} @@ -11,9 +11,9 @@ class StatusRelationshipsPresenter else status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq - @reblogs_map = Status.reblogs_map(status_ids, current_account_id) - @favourites_map = Status.favourites_map(status_ids, current_account_id) - @mutes_map = Status.mutes_map(conversation_ids, current_account_id) + @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(reblogs_map) + @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(favourites_map) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(mutes_map) end end end From 72bd73f605a7253daf445f76425da3182a1f669c Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 14 Jul 2017 22:31:25 +0200 Subject: [PATCH 28/35] 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(-) diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js index 2996d4dc8d8..e2fe1fed7eb 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 5bc46e8eeb4..86e8386bdad 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 0420a2bedc6..fc797a50865 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%; } From de397f3bc1f1c7e632d0db0754711beb6ce04664 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 22:31:38 +0200 Subject: [PATCH 29/35] Fix subsequent replies to unresolved status not being filtered from home (#4190) Resolves #4177 - smaller changeset --- app/lib/feed_manager.rb | 7 ++++--- spec/lib/feed_manager_spec.rb | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index c507f263653..b1ae1108402 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -93,7 +93,7 @@ class FeedManager end def filter_from_home?(status, receiver_id) - return true if status.reply? && status.in_reply_to_id.nil? + return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) check_for_mutes = [status.account_id] check_for_mutes.concat([status.reblog.account_id]) if status.reblog? @@ -120,12 +120,13 @@ class FeedManager end def filter_from_mentions?(status, receiver_id) + return true if receiver_id == status.account_id + check_for_blocks = [status.account_id] check_for_blocks.concat(status.mentions.pluck(:account_id)) check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil? - should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself - should_filter ||= Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # or it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked + should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them should_filter diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 4bdc96866d1..22439cf35de 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -81,6 +81,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true end + it 'returns true for the second reply by followee to a non-federated status' do + reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice) + second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice) + bob.follow!(alice) + expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true + end + it 'returns false for status by followee mentioning another account' do bob.follow!(alice) status = PostStatusService.new.call(alice, 'Hey @jeff') From cd9b2ab2f70b6c1da5d0abeaa88eecdfc1b41f78 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 23:01:20 +0200 Subject: [PATCH 30/35] Fix #2672 - Connect signed PuSH subscription requests to instance domain (#4205) * Fix #2672 - Connect signed PuSH subscription requests to instance domain Resolves #2739 * Fix return of locate_subscription * Fix tests --- app/controllers/api/push_controller.rb | 8 +++++++- app/models/subscription.rb | 2 +- app/services/pubsubhubbub/subscribe_service.rb | 16 +++++++++++++--- app/workers/pubsubhubbub/distribution_worker.rb | 8 ++++---- ...20170714184731_add_domain_to_subscriptions.rb | 5 +++++ db/schema.rb | 3 ++- spec/controllers/api/push_controller_spec.rb | 1 + 7 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20170714184731_add_domain_to_subscriptions.rb diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb index 951867140ca..e04d19125b8 100644 --- a/app/controllers/api/push_controller.rb +++ b/app/controllers/api/push_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::PushController < Api::BaseController + include SignatureVerification + def update response, status = process_push_request render plain: response, status: status @@ -11,7 +13,7 @@ class Api::PushController < Api::BaseController def process_push_request case hub_mode when 'subscribe' - Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds) + Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain) when 'unsubscribe' Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback) else @@ -57,6 +59,10 @@ class Api::PushController < Api::BaseController TagManager.instance.web_domain?(hub_topic_domain) end + def verified_domain + return signed_request_account.domain if signed_request_account + end + def hub_topic_domain hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '') end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index d9d5024a964..bf643c1f9ca 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - # == Schema Information # # Table name: subscriptions @@ -13,6 +12,7 @@ # created_at :datetime not null # updated_at :datetime not null # last_successful_delivery_at :datetime +# domain :string # class Subscription < ApplicationRecord diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb index eeb7ab25867..2dba05b12f1 100644 --- a/app/services/pubsubhubbub/subscribe_service.rb +++ b/app/services/pubsubhubbub/subscribe_service.rb @@ -3,13 +3,15 @@ class Pubsubhubbub::SubscribeService < BaseService URL_PATTERN = /\A#{URI.regexp(%w(http https))}\z/ - attr_reader :account, :callback, :secret, :lease_seconds + attr_reader :account, :callback, :secret, + :lease_seconds, :domain - def call(account, callback, secret, lease_seconds) + def call(account, callback, secret, lease_seconds, verified_domain = nil) @account = account @callback = Addressable::URI.parse(callback).normalize.to_s @secret = secret @lease_seconds = lease_seconds + @domain = verified_domain process_subscribe end @@ -56,6 +58,14 @@ class Pubsubhubbub::SubscribeService < BaseService end def locate_subscription - Subscription.where(account: account, callback_url: callback).first_or_create!(account: account, callback_url: callback) + subscription = Subscription.find_by(account: account, callback_url: callback) + + if subscription.nil? + subscription = Subscription.new(account: account, callback_url: callback) + end + + subscription.domain = domain + subscription.save! + subscription end end diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index b41cec90d32..7592354cc25 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -35,16 +35,16 @@ class Pubsubhubbub::DistributionWorker @payload = AtomSerializer.render(AtomSerializer.new.feed(@account, stream_entries)) @domains = @account.followers.domains - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url) }) do |subscription| + Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription| [subscription.id, @payload] end end def active_subscriptions - Subscription.where(account: @account).active.select('id, callback_url') + Subscription.where(account: @account).active.select('id, callback_url, domain') end - def allowed_to_receive?(callback_url) - @domains.include?(Addressable::URI.parse(callback_url).host) + def allowed_to_receive?(callback_url, domain) + (!domain.nil? && @domains.include?(domain)) || @domains.include?(Addressable::URI.parse(callback_url).host) end end diff --git a/db/migrate/20170714184731_add_domain_to_subscriptions.rb b/db/migrate/20170714184731_add_domain_to_subscriptions.rb new file mode 100644 index 00000000000..7c01a64f572 --- /dev/null +++ b/db/migrate/20170714184731_add_domain_to_subscriptions.rb @@ -0,0 +1,5 @@ +class AddDomainToSubscriptions < ActiveRecord::Migration[5.1] + def change + add_column :subscriptions, :domain, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index b2c59a0f666..5ec78a7c98f 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: 20170713190709) do +ActiveRecord::Schema.define(version: 20170714184731) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -326,6 +326,7 @@ ActiveRecord::Schema.define(version: 20170713190709) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "last_successful_delivery_at" + t.string "domain" t.index ["account_id", "callback_url"], name: "index_subscriptions_on_account_id_and_callback_url", unique: true end diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb index 18bfa70e57e..647698bd196 100644 --- a/spec/controllers/api/push_controller_spec.rb +++ b/spec/controllers/api/push_controller_spec.rb @@ -21,6 +21,7 @@ RSpec.describe Api::PushController, type: :controller do 'https://callback.host/api', 'as1234df', '3600', + nil ) expect(response).to have_http_status(:success) end From 3fbf1bf35acf89d1e397fa2e632529bf5105fe02 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 15 Jul 2017 00:49:34 +0200 Subject: [PATCH 31/35] Fix #3773 - Pin favourites column (#4201) --- .../features/favourited_statuses/index.js | 64 +++++++++++++------ .../features/ui/components/columns_area.js | 3 +- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 8cef6a1e4ee..d9ad9bc1f95 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 ef9a1552236..ae3f9261b8c 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 { From 8c45cd0e3683b528b65f416681c8272d5650f32d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 15 Jul 2017 03:01:39 +0200 Subject: [PATCH 32/35] Improve ActivityPub representations (#3844) * Improve webfinger templates and make tests more flexible * Clean up AS2 representation of actor * Refactor outbox * Create activities representation * Add representations of followers/following collections, do not redirect /users/:username route if format is empty * Remove unused translations * ActivityPub endpoint for single statuses, add ActivityPub::TagManager for better URL/URI generation * Add ActivityPub::TagManager#to * Represent all attachments as Document instead of Image/Video specifically (Because for remote ones we may not know for sure) Add mentions and hashtags representation to AP notes * Add AP-resolvable hashtag URIs * Use ActiveModelSerializers for ActivityPub * Clean up unused translations * Separate route for object and activity * Adjust cc/to matrices * Add to/cc to activities, ensure announce activity embeds target status and not the wrapper status, add "id" to all collections --- app/controllers/accounts_controller.rb | 4 +- .../activitypub/outboxes_controller.rb | 28 ++++ .../api/activitypub/activities_controller.rb | 27 --- .../api/activitypub/notes_controller.rb | 19 --- .../api/activitypub/outbox_controller.rb | 69 -------- .../follower_accounts_controller.rb | 20 +++ .../following_accounts_controller.rb | 20 +++ app/controllers/statuses_controller.rb | 18 +- app/controllers/tags_controller.rb | 22 ++- .../activitystreams2_builder_helper.rb | 8 - app/lib/activitypub/adapter.rb | 13 ++ app/lib/activitypub/tag_manager.rb | 69 ++++++++ .../activitypub/collection_presenter.rb | 5 + .../activitypub/activity_serializer.rb | 27 +++ .../activitypub/actor_serializer.rb | 53 ++++++ .../activitypub/collection_serializer.rb | 26 +++ .../activitypub/note_serializer.rb | 106 ++++++++++++ app/views/accounts/show.activitystreams2.rabl | 9 - .../activitypub/base.activitystreams2.rabl | 1 - .../intransient.activitystreams2.rabl | 3 - .../types/announce.activitystreams2.rabl | 3 - .../types/collection.activitystreams2.rabl | 3 - .../types/create.activitystreams2.rabl | 3 - .../types/note.activitystreams2.rabl | 3 - .../ordered_collection.activitystreams2.rabl | 3 - ...ered_collection_page.activitystreams2.rabl | 3 - .../types/person.activitystreams2.rabl | 3 - .../_show_status.activitystreams2.rabl | 4 - ...show_status_announce.activitystreams2.rabl | 8 - .../show_status_create.activitystreams2.rabl | 8 - .../notes/show.activitystreams2.rabl | 11 -- .../outbox/show.activitystreams2.rabl | 12 -- .../outbox/show_page.activitystreams2.rabl | 16 -- app/views/well_known/webfinger/show.json.rabl | 6 +- app/views/well_known/webfinger/show.xml.ruby | 5 +- config/initializers/inflections.rb | 2 + config/initializers/mime_types.rb | 5 +- config/locales/ca.yml | 9 - config/locales/en.yml | 9 - config/locales/fa.yml | 9 - config/locales/fr.yml | 9 - config/locales/he.yml | 9 - config/locales/id.yml | 9 - config/locales/ja.yml | 9 - config/locales/ko.yml | 9 - config/locales/no.yml | 9 - config/locales/oc.yml | 9 - config/locales/pl.yml | 9 - config/locales/pt-BR.yml | 9 - config/locales/pt.yml | 9 - config/locales/th.yml | 9 - config/locales/tr.yml | 9 - config/locales/zh-CN.yml | 9 - config/locales/zh-HK.yml | 9 - config/routes.rb | 16 +- spec/controllers/accounts_controller_spec.rb | 2 +- .../activitypub/activities_controller_spec.rb | 69 -------- .../api/activitypub/notes_controller_spec.rb | 73 -------- .../api/activitypub/outbox_controller_spec.rb | 156 ------------------ .../well_known/webfinger_controller_spec.rb | 39 ++--- .../activitystreams2_builder_helper_spec.rb | 15 -- 61 files changed, 443 insertions(+), 725 deletions(-) create mode 100644 app/controllers/activitypub/outboxes_controller.rb delete mode 100644 app/controllers/api/activitypub/activities_controller.rb delete mode 100644 app/controllers/api/activitypub/notes_controller.rb delete mode 100644 app/controllers/api/activitypub/outbox_controller.rb delete mode 100644 app/helpers/activitystreams2_builder_helper.rb create mode 100644 app/lib/activitypub/adapter.rb create mode 100644 app/lib/activitypub/tag_manager.rb create mode 100644 app/presenters/activitypub/collection_presenter.rb create mode 100644 app/serializers/activitypub/activity_serializer.rb create mode 100644 app/serializers/activitypub/actor_serializer.rb create mode 100644 app/serializers/activitypub/collection_serializer.rb create mode 100644 app/serializers/activitypub/note_serializer.rb delete mode 100644 app/views/accounts/show.activitystreams2.rabl delete mode 100644 app/views/activitypub/base.activitystreams2.rabl delete mode 100644 app/views/activitypub/intransient.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/announce.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/collection.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/create.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/note.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/ordered_collection.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/ordered_collection_page.activitystreams2.rabl delete mode 100644 app/views/activitypub/types/person.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/activities/_show_status.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/activities/show_status_announce.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/activities/show_status_create.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/notes/show.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/outbox/show.activitystreams2.rabl delete mode 100644 app/views/api/activitypub/outbox/show_page.activitystreams2.rabl delete mode 100644 spec/controllers/api/activitypub/activities_controller_spec.rb delete mode 100644 spec/controllers/api/activitypub/notes_controller_spec.rb delete mode 100644 spec/controllers/api/activitypub/outbox_controller_spec.rb delete mode 100644 spec/helpers/activitystreams2_builder_helper_spec.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 69b520df1e9..a95aabf1d81 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -16,7 +16,9 @@ class AccountsController < ApplicationController render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a)) end - format.activitystreams2 + format.json do + render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter + end end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb new file mode 100644 index 00000000000..6a58ccf2431 --- /dev/null +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ActivityPub::OutboxesController < Api::BaseController + before_action :set_account + + def show + @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = cache_collection(@statuses, Status) + + render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + end + + private + + def set_account + @account = Account.find_local!(params[:account_username]) + end + + def outbox_presenter + ActivityPub::CollectionPresenter.new( + id: account_outbox_url(@account), + type: :ordered, + current: account_outbox_url(@account), + size: @account.statuses_count, + items: @statuses + ) + end +end diff --git a/app/controllers/api/activitypub/activities_controller.rb b/app/controllers/api/activitypub/activities_controller.rb deleted file mode 100644 index a880ee92f27..00000000000 --- a/app/controllers/api/activitypub/activities_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class Api::ActivityPub::ActivitiesController < Api::BaseController - include Authorization - - # before_action :set_follow, only: [:show_follow] - before_action :set_status, only: [:show_status] - - respond_to :activitystreams2 - - # Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity. - def show_status - authorize @status, :show? - - if @status.reblog? - render :show_status_announce - else - render :show_status_create - end - end - - private - - def set_status - @status = Status.find(params[:id]) - end -end diff --git a/app/controllers/api/activitypub/notes_controller.rb b/app/controllers/api/activitypub/notes_controller.rb deleted file mode 100644 index 96652b8791c..00000000000 --- a/app/controllers/api/activitypub/notes_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class Api::ActivityPub::NotesController < Api::BaseController - include Authorization - - before_action :set_status - - respond_to :activitystreams2 - - def show - authorize @status, :show? - end - - private - - def set_status - @status = Status.find(params[:id]) - end -end diff --git a/app/controllers/api/activitypub/outbox_controller.rb b/app/controllers/api/activitypub/outbox_controller.rb deleted file mode 100644 index 1af04cb54fe..00000000000 --- a/app/controllers/api/activitypub/outbox_controller.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -class Api::ActivityPub::OutboxController < Api::BaseController - before_action :set_account - - respond_to :activitystreams2 - - def show - if params[:max_id] || params[:since_id] - show_outbox_page - else - show_base_outbox - end - end - - private - - def show_base_outbox - @statuses = Status.as_outbox_timeline(@account) - @statuses = cache_collection(@statuses) - - set_maps(@statuses) - - set_first_last_page(@statuses) - - render :show - end - - def show_outbox_page - all_statuses = Status.as_outbox_timeline(@account) - @statuses = all_statuses.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - - all_statuses = cache_collection(all_statuses) - @statuses = cache_collection(@statuses) - - set_maps(@statuses) - - set_first_last_page(all_statuses) - - @next_page_url = api_activitypub_outbox_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty? - @prev_page_url = api_activitypub_outbox_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty? - - @paginated = @next_page_url || @prev_page_url - @part_of_url = api_activitypub_outbox_url - - set_pagination_headers(@next_page_url, @prev_page_url) - - render :show_page - end - - def cache_collection(raw) - super(raw, Status) - end - - def set_account - @account = Account.find(params[:id]) - end - - def set_first_last_page(statuses) # rubocop:disable Style/AccessorMethodName - return if statuses.empty? - - @first_page_url = api_activitypub_outbox_url(max_id: statuses.first.id + 1) - @last_page_url = api_activitypub_outbox_url(since_id: statuses.last.id - 1) - end - - def pagination_params(core_params) - params.permit(:local, :limit).merge(core_params) - end -end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1e7c7c40663..e58c5ad46e6 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,5 +5,25 @@ class FollowerAccountsController < ApplicationController def index @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) + + respond_to do |format| + format.html + + format.json do + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + end + end + end + + private + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: account_followers_url(@account), + type: :ordered, + current: account_followers_url(@account), + size: @account.followers_count, + items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) } + ) end end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index f4488eef5ea..69f29cd7088 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,5 +5,25 @@ class FollowingAccountsController < ApplicationController def index @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) + + respond_to do |format| + format.html + + format.json do + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + end + end + end + + private + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: account_following_index_url(@account), + type: :ordered, + current: account_following_index_url(@account), + size: @account.following_count, + items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) } + ) end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 59c9d0a87fa..8e0ce0ec3b0 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -11,10 +11,22 @@ class StatusesController < ApplicationController before_action :check_account_suspension def show - @ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] - @descendants = cache_collection(@status.descendants(current_account), Status) + respond_to do |format| + format.html do + @ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] + @descendants = cache_collection(@status.descendants(current_account), Status) - render 'stream_entries/show' + render 'stream_entries/show' + end + + format.json do + render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter + end + end + end + + def activity + render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end private diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 53149edf0a4..8bcce9e13ce 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -5,7 +5,27 @@ class TagsController < ApplicationController def show @tag = Tag.find_by!(name: params[:id].downcase) - @statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id]) + @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id]) @statuses = cache_collection(@statuses, Status) + + respond_to do |format| + format.html + + format.json do + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + end + end + end + + private + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: tag_url(@tag), + type: :ordered, + current: tag_url(@tag), + size: @tag.statuses.count, + items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } + ) end end diff --git a/app/helpers/activitystreams2_builder_helper.rb b/app/helpers/activitystreams2_builder_helper.rb deleted file mode 100644 index 717b470f0da..00000000000 --- a/app/helpers/activitystreams2_builder_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module Activitystreams2BuilderHelper - # Gets a usable name for an account, using display name or username. - def account_name(account) - account.display_name.presence || account.username - end -end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb new file mode 100644 index 00000000000..0a70207bc47 --- /dev/null +++ b/app/lib/activitypub/adapter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base + def self.default_key_transform + :camel_lower + end + + def serializable_hash(options = nil) + options = serialization_options(options) + serialized_hash = { '@context': 'https://www.w3.org/ns/activitystreams' }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) + self.class.transform_key_casing!(serialized_hash, instance_options) + end +end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb new file mode 100644 index 00000000000..ec42bcad30f --- /dev/null +++ b/app/lib/activitypub/tag_manager.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'singleton' + +class ActivityPub::TagManager + include Singleton + include RoutingHelper + + COLLECTIONS = { + public: 'https://www.w3.org/ns/activitystreams#Public', + }.freeze + + def url_for(target) + return target.url if target.respond_to?(:local?) && !target.local? + + case target.object_type + when :person + short_account_url(target) + when :note, :comment, :activity + short_account_status_url(target.account, target) + end + end + + def uri_for(target) + return target.uri if target.respond_to?(:local?) && !target.local? + + case target.object_type + when :person + account_url(target) + when :note, :comment, :activity + account_status_url(target.account, target) + end + end + + # Primary audience of a status + # Public statuses go out to primarily the public collection + # Unlisted and private statuses go out primarily to the followers collection + # Others go out only to the people they mention + def to(status) + case status.visibility + when 'public' + [COLLECTIONS[:public]] + when 'unlisted', 'private' + [account_followers_url(status.account)] + when 'direct' + status.mentions.map { |mention| uri_for(mention.account) } + end + end + + # Secondary audience of a status + # Public statuses go out to followers as well + # Unlisted statuses go to the public as well + # Both of those and private statuses also go to the people mentioned in them + # Direct ones don't have a secondary audience + def cc(status) + cc = [] + + case status.visibility + when 'public' + cc << account_followers_url(status.account) + when 'unlisted' + cc << COLLECTIONS[:public] + end + + cc.concat(status.mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility? + + cc + end +end diff --git a/app/presenters/activitypub/collection_presenter.rb b/app/presenters/activitypub/collection_presenter.rb new file mode 100644 index 00000000000..6bae2955ea5 --- /dev/null +++ b/app/presenters/activitypub/collection_presenter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ActivityPub::CollectionPresenter < ActiveModelSerializers::Model + attributes :id, :type, :current, :size, :items +end diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb new file mode 100644 index 00000000000..69e2160c5d4 --- /dev/null +++ b/app/serializers/activitypub/activity_serializer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ActivityPub::ActivitySerializer < ActiveModel::Serializer + attributes :id, :type, :actor, :to, :cc + + has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer + + def id + [ActivityPub::TagManager.instance.uri_for(object), '/activity'].join + end + + def type + object.reblog? ? 'Announce' : 'Create' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.to(object) + end + + def cc + ActivityPub::TagManager.instance.cc(object) + end +end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb new file mode 100644 index 00000000000..56806152eed --- /dev/null +++ b/app/serializers/activitypub/actor_serializer.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ActivityPub::ActorSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :type, :following, :followers, + :inbox, :outbox, :preferred_username, + :name, :summary, :icon, :image + + def id + account_url(object) + end + + def type + 'Person' + end + + def following + account_following_index_url(object) + end + + def followers + account_followers_url(object) + end + + def inbox + nil + end + + def outbox + account_outbox_url(object) + end + + def preferred_username + object.username + end + + def name + object.display_name + end + + def summary + Formatter.instance.simplified_format(object) + end + + def icon + full_asset_url(object.avatar.url(:original)) + end + + def image + full_asset_url(object.header.url(:original)) + end +end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb new file mode 100644 index 00000000000..baaba76543c --- /dev/null +++ b/app/serializers/activitypub/collection_serializer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ActivityPub::CollectionSerializer < ActiveModel::Serializer + def self.serializer_for(model, options) + return ActivityPub::ActivitySerializer if model.class.name == 'Status' + super + end + + attributes :id, :type, :total_items, + :current + + has_many :items, key: :ordered_items + + def type + case object.type + when :ordered + 'OrderedCollection' + else + 'Collection' + end + end + + def total_items + object.size + end +end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb new file mode 100644 index 00000000000..ffdc6175d41 --- /dev/null +++ b/app/serializers/activitypub/note_serializer.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class ActivityPub::NoteSerializer < ActiveModel::Serializer + attributes :id, :type, :summary, :content, + :in_reply_to, :published, :url, + :actor, :to, :cc, :sensitive + + has_many :media_attachments, key: :attachment + has_many :virtual_tags, key: :tag + + def id + ActivityPub::TagManager.instance.uri_for(object) + end + + def type + 'Note' + end + + def summary + object.spoiler_text.presence + end + + def content + Formatter.instance.format(object) + end + + def in_reply_to + ActivityPub::TagManager.instance.uri_for(object.thread) if object.reply? + end + + def published + object.created_at.iso8601 + end + + def url + ActivityPub::TagManager.instance.url_for(object) + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.to(object) + end + + def cc + ActivityPub::TagManager.instance.cc(object) + end + + def virtual_tags + object.mentions + object.tags + end + + class MediaAttachmentSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :media_type, :url + + def type + 'Document' + end + + def media_type + object.file_content_type + end + + def url + object.local? ? full_asset_url(object.file.url(:original, false)) : object.remote_url + end + end + + class MentionSerializer < ActiveModel::Serializer + attributes :type, :href, :name + + def type + 'Mention' + end + + def href + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def name + "@#{object.account.acct}" + end + end + + class TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :href, :name + + def type + 'Hashtag' + end + + def href + tag_url(object) + end + + def name + "##{object.name}" + end + end +end diff --git a/app/views/accounts/show.activitystreams2.rabl b/app/views/accounts/show.activitystreams2.rabl deleted file mode 100644 index 2c0a4ad3a08..00000000000 --- a/app/views/accounts/show.activitystreams2.rabl +++ /dev/null @@ -1,9 +0,0 @@ -extends 'activitypub/types/person.activitystreams2.rabl' - -object @account - -attributes display_name: :name, username: :preferredUsername, note: :summary - -node(:icon) { |account| full_asset_url(account.avatar.url(:original)) } -node(:image) { |account| full_asset_url(account.header.url(:original)) } -node(:outbox) { |account| api_activitypub_outbox_url(account.id) } diff --git a/app/views/activitypub/base.activitystreams2.rabl b/app/views/activitypub/base.activitystreams2.rabl deleted file mode 100644 index c5e94997a1b..00000000000 --- a/app/views/activitypub/base.activitystreams2.rabl +++ /dev/null @@ -1 +0,0 @@ -node(:'@context') { 'https://www.w3.org/ns/activitystreams' } diff --git a/app/views/activitypub/intransient.activitystreams2.rabl b/app/views/activitypub/intransient.activitystreams2.rabl deleted file mode 100644 index 968e451c266..00000000000 --- a/app/views/activitypub/intransient.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/base.activitystreams2.rabl' - -node(:id) { request.original_url } diff --git a/app/views/activitypub/types/announce.activitystreams2.rabl b/app/views/activitypub/types/announce.activitystreams2.rabl deleted file mode 100644 index 4a29aa13425..00000000000 --- a/app/views/activitypub/types/announce.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/intransient.activitystreams2.rabl' - -node(:type) { 'Announce' } diff --git a/app/views/activitypub/types/collection.activitystreams2.rabl b/app/views/activitypub/types/collection.activitystreams2.rabl deleted file mode 100644 index cc0e532b71e..00000000000 --- a/app/views/activitypub/types/collection.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/intransient.activitystreams2.rabl' - -node(:type) { 'Collection' } diff --git a/app/views/activitypub/types/create.activitystreams2.rabl b/app/views/activitypub/types/create.activitystreams2.rabl deleted file mode 100644 index e41a056a780..00000000000 --- a/app/views/activitypub/types/create.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/intransient.activitystreams2.rabl' - -node(:type) { 'Create' } diff --git a/app/views/activitypub/types/note.activitystreams2.rabl b/app/views/activitypub/types/note.activitystreams2.rabl deleted file mode 100644 index 39c74d4bafa..00000000000 --- a/app/views/activitypub/types/note.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/intransient.activitystreams2.rabl' - -node(:type) { 'Note' } diff --git a/app/views/activitypub/types/ordered_collection.activitystreams2.rabl b/app/views/activitypub/types/ordered_collection.activitystreams2.rabl deleted file mode 100644 index 2cda6f4d016..00000000000 --- a/app/views/activitypub/types/ordered_collection.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/types/collection.activitystreams2.rabl' - -node(:type) { 'OrderedCollection' } diff --git a/app/views/activitypub/types/ordered_collection_page.activitystreams2.rabl b/app/views/activitypub/types/ordered_collection_page.activitystreams2.rabl deleted file mode 100644 index 9937d11e9cb..00000000000 --- a/app/views/activitypub/types/ordered_collection_page.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/types/ordered_collection.activitystreams2.rabl' - -node(:type) { 'OrderedCollectionPage' } diff --git a/app/views/activitypub/types/person.activitystreams2.rabl b/app/views/activitypub/types/person.activitystreams2.rabl deleted file mode 100644 index 487a60791f3..00000000000 --- a/app/views/activitypub/types/person.activitystreams2.rabl +++ /dev/null @@ -1,3 +0,0 @@ -extends 'activitypub/intransient.activitystreams2.rabl' - -node(:type) { 'Person' } diff --git a/app/views/api/activitypub/activities/_show_status.activitystreams2.rabl b/app/views/api/activitypub/activities/_show_status.activitystreams2.rabl deleted file mode 100644 index 472bf5dbd18..00000000000 --- a/app/views/api/activitypub/activities/_show_status.activitystreams2.rabl +++ /dev/null @@ -1,4 +0,0 @@ -object @status - -node(:actor) { |status| TagManager.instance.url_for(status.account) } -node(:published) { |status| status.created_at.to_time.xmlschema } \ No newline at end of file diff --git a/app/views/api/activitypub/activities/show_status_announce.activitystreams2.rabl b/app/views/api/activitypub/activities/show_status_announce.activitystreams2.rabl deleted file mode 100644 index 44ac1ba2fac..00000000000 --- a/app/views/api/activitypub/activities/show_status_announce.activitystreams2.rabl +++ /dev/null @@ -1,8 +0,0 @@ -extends 'activitypub/types/announce.activitystreams2.rabl' -extends 'api/activitypub/activities/_show_status.activitystreams2.rabl' - -object @status - -node(:name) { |status| t('activitypub.activity.announce.name', account_name: account_name(status.account)) } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:object) { |status| api_activitypub_status_url(status.reblog_of_id) } diff --git a/app/views/api/activitypub/activities/show_status_create.activitystreams2.rabl b/app/views/api/activitypub/activities/show_status_create.activitystreams2.rabl deleted file mode 100644 index ff4d39ecae4..00000000000 --- a/app/views/api/activitypub/activities/show_status_create.activitystreams2.rabl +++ /dev/null @@ -1,8 +0,0 @@ -extends 'activitypub/types/create.activitystreams2.rabl' -extends 'api/activitypub/activities/_show_status.activitystreams2.rabl' - -object @status - -node(:name) { |status| t('activitypub.activity.create.name', account_name: account_name(status.account)) } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:object) { |status| api_activitypub_note_url(status) } diff --git a/app/views/api/activitypub/notes/show.activitystreams2.rabl b/app/views/api/activitypub/notes/show.activitystreams2.rabl deleted file mode 100644 index d962f4438e0..00000000000 --- a/app/views/api/activitypub/notes/show.activitystreams2.rabl +++ /dev/null @@ -1,11 +0,0 @@ -extends 'activitypub/types/note.activitystreams2.rabl' - -object @status - -attributes :content - -node(:name) { |status| status.content } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:attributedTo) { |status| TagManager.instance.url_for(status.account) } -node(:inReplyTo) { |status| api_activitypub_note_url(status.thread) } if @status.thread -node(:published) { |status| status.created_at.to_time.xmlschema } diff --git a/app/views/api/activitypub/outbox/show.activitystreams2.rabl b/app/views/api/activitypub/outbox/show.activitystreams2.rabl deleted file mode 100644 index 273b15e82d0..00000000000 --- a/app/views/api/activitypub/outbox/show.activitystreams2.rabl +++ /dev/null @@ -1,12 +0,0 @@ -extends 'activitypub/types/ordered_collection.activitystreams2.rabl' - -object @account - -node(:totalItems) { @statuses.count } -node(:current) { @first_page_url } if @first_page_url -node(:first) { @first_page_url } if @first_page_url -node(:last) { @last_page_url } if @last_page_url - -node(:name) { |account| t('activitypub.outbox.name', account_name: account_name(account)) } -node(:summary) { |account| t('activitypub.outbox.summary', account_name: account_name(account)) } -node(:updated) { |account| (@statuses.empty? ? account.created_at.to_time : @statuses.first.updated_at.to_time).xmlschema } diff --git a/app/views/api/activitypub/outbox/show_page.activitystreams2.rabl b/app/views/api/activitypub/outbox/show_page.activitystreams2.rabl deleted file mode 100644 index b6433ccf2b8..00000000000 --- a/app/views/api/activitypub/outbox/show_page.activitystreams2.rabl +++ /dev/null @@ -1,16 +0,0 @@ -extends 'activitypub/types/ordered_collection_page.activitystreams2.rabl' - -object @account - -node(:items) do - @statuses.map { |status| api_activitypub_status_url(status) } -end - -node(:next) { @next_page_url } if @next_page_url -node(:prev) { @prev_page_url } if @prev_page_url -node(:current) { @first_page_url } if @first_page_url -node(:first) { @first_page_url } if @first_page_url -node(:last) { @last_page_url } if @last_page_url -node(:partOf) { @part_of_url } if @part_of_url - -node(:updated) { |account| (@statuses.empty? ? account.created_at.to_time : @statuses.first.updated_at.to_time).xmlschema } diff --git a/app/views/well_known/webfinger/show.json.rabl b/app/views/well_known/webfinger/show.json.rabl index 123d1d11ac1..af11cd20700 100644 --- a/app/views/well_known/webfinger/show.json.rabl +++ b/app/views/well_known/webfinger/show.json.rabl @@ -3,14 +3,14 @@ object @account node(:subject) { @canonical_account_uri } node(:aliases) do - [TagManager.instance.url_for(@account), TagManager.instance.uri_for(@account)] + [short_account_url(@account), account_url(@account)] end node(:links) do [ - { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account) }, + { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: account_url(@account) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }, - { rel: 'self', type: 'application/activity+json', href: TagManager.instance.url_for(@account) }, + { rel: 'self', type: 'application/activity+json', href: account_url(@account) }, { rel: 'salmon', href: api_salmon_url(@account.id) }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" }, diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index fc0ab5b84d1..844742d68a0 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -1,10 +1,11 @@ Nokogiri::XML::Builder.new do |xml| xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do xml.Subject @canonical_account_uri - xml.Alias TagManager.instance.url_for(@account) - xml.Alias TagManager.instance.uri_for(@account) + xml.Alias short_account_url(@account) + xml.Alias account_url(@account) xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account)) xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom')) + xml.Link(rel: 'self', type: 'application/activity+json', href: account_url(@account)) xml.Link(rel: 'salmon', href: api_salmon_url(@account.id)) xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}") xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}") diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index a7b1ef690f2..26275d0928c 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -14,4 +14,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'StatsD' inflect.acronym 'OEmbed' inflect.acronym 'ActivityPub' + inflect.acronym 'PubSubHubbub' + inflect.acronym 'ActivityStreams' end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index b1b73c84655..30e91ad63f5 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,5 +1,4 @@ # Be sure to restart your server when you modify this file. -Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/jrd+json ) -Mime::Type.register "text/xml", :xml, %w( application/xml application/atom+xml application/xrd+xml ) -Mime::Type.register "application/activity+json", :activitystreams2 +Mime::Type.register 'application/json', :json, %w(text/x-json application/jsonrequest application/jrd+json application/activity+json) +Mime::Type.register 'text/xml', :xml, %w(application/xml application/atom+xml application/xrd+xml) diff --git a/config/locales/ca.yml b/config/locales/ca.yml index f63aee3e68b..0ba893a122a 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -30,15 +30,6 @@ ca: remote_follow: Seguir reserved_username: El nom d'usuari està reservat unfollow: Deixar de seguir - activitypub: - activity: - announce: - name: "%{account_name} shared an activity." - create: - name: "%{account_name} created a note." - outbox: - name: "%{account_name}'s Outbox" - summary: A collection of activities from user %{account_name}. admin: accounts: are_you_sure: Estàs segur? diff --git a/config/locales/en.yml b/config/locales/en.yml index 79efddfadb0..be1f15e25a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -44,15 +44,6 @@ en: remote_follow: Remote follow reserved_username: The username is reserved unfollow: Unfollow - activitypub: - activity: - announce: - name: "%{account_name} shared an activity." - create: - name: "%{account_name} created a note." - outbox: - name: "%{account_name}'s Outbox" - summary: A collection of activities from user %{account_name}. admin: accounts: are_you_sure: Are you sure? diff --git a/config/locales/fa.yml b/config/locales/fa.yml index ade76d6702e..218d859bb44 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -29,15 +29,6 @@ fa: posts: نوشته remote_follow: پیگیری غیرمستقیم unfollow: پایان پیگیری - activitypub: - activity: - announce: - name: "%{account_name} فعالیتی آغاز کرد." - create: - name: "%{account_name} یادداشتی نوشت." - outbox: - name: صندوق خروجی %{account_name} - summary: مجموعه‌ای از فعالیت‌های کاربر %{account_name}. admin: accounts: are_you_sure: آیا مطمئن هستید؟ diff --git a/config/locales/fr.yml b/config/locales/fr.yml index cba21765167..65e681b205e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -30,15 +30,6 @@ fr: remote_follow: Suivre à distance reserved_username: Ce nom d’utilisateur⋅ice est réservé unfollow: Ne plus suivre - activitypub: - activity: - announce: - name: "%{account_name} a partagé une activité." - create: - name: "%{account_name} a créé une note." - outbox: - name: Boîte d’envoi de %{account_name} - summary: Liste d’activités de %{account_name} admin: accounts: are_you_sure: Êtes-vous certain⋅e ? diff --git a/config/locales/he.yml b/config/locales/he.yml index 21f8f1dc4cf..251b6914e5c 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -29,15 +29,6 @@ he: posts: הודעות remote_follow: מעקב מרחוק unfollow: הפסקת מעקב - activitypub: - activity: - announce: - name: הודעה שותפה על ידי %{account_name}. - create: - name: הודעה חדשה מאת %{account_name}. - outbox: - name: תיבת הדוא"ל היוצא של %{account_name} - summary: אוסף הפעילויות של %{account_name}. admin: accounts: are_you_sure: בטוח? diff --git a/config/locales/id.yml b/config/locales/id.yml index e3fe9633177..7bda52c789a 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -29,15 +29,6 @@ id: posts: Postingan remote_follow: Mengikuti unfollow: Berhenti mengikuti - activitypub: - activity: - announce: - name: "%{account_name} membagikan aktivitas." - create: - name: "%{account_name} membuat catatan." - outbox: - name: "%{account_name} Outbox" - summary: Koleksi aktivitas dari pengguna %{account_name}. admin: accounts: are_you_sure: Anda yakin? diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 37d82a20557..fda87526de9 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -30,15 +30,6 @@ ja: remote_follow: リモートフォロー reserved_username: このユーザー名は予約されています。 unfollow: フォロー解除 - activitypub: - activity: - announce: - name: "%{account_name} さんがアクティビティをシェアしました" - create: - name: "%{account_name} さんがノートを作成しました" - outbox: - name: "%{account_name} さんの送信トレイ" - summary: "%{account_name} さんからのアクティビティコレクション" admin: accounts: are_you_sure: 本当に実行しますか? diff --git a/config/locales/ko.yml b/config/locales/ko.yml index bafc1999344..c7c310cfed2 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -30,15 +30,6 @@ ko: remote_follow: 리모트 팔로우 reserved_username: 이 아이디는 예약되어 있습니다. unfollow: 팔로우 해제 - activitypub: - activity: - announce: - name: "%{account_name} 님이 액티비티를 공유했습니다" - create: - name: "%{account_name} 님이 노트를 작성했습니다" - outbox: - name: "%{account_name} 님의 송신함" - summary: "%{account_name} 님의 액티비티 모음" admin: accounts: are_you_sure: 정말로 실행하시겠습니까? diff --git a/config/locales/no.yml b/config/locales/no.yml index 004e1ff8032..cf94524d294 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -29,15 +29,6 @@ posts: Poster remote_follow: Følg fra andre instanser unfollow: Avfølg - activitypub: - activity: - announce: - name: "%{account_name} delte en aktivitet." - create: - name: "%{account_name} laget en aktivitet." - outbox: - name: "%{account_name} sin utboks" - summary: En samling aktiviteter fra brukeren %{account_name}. admin: accounts: are_you_sure: Er du sikker? diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 91a6ca7910d..2eb85be5852 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -29,15 +29,6 @@ oc: posts: Estatuts remote_follow: Sègre a distància unfollow: Quitar de sègre - activitypub: - activity: - announce: - name: "%{account_name} a partejat una activitat." - create: - name: "%{account_name} a creat una nòta." - outbox: - name: "%{account_name}'s Outbox" - summary: A collection of activities from user %{account_name}. admin: accounts: are_you_sure: Sètz segur ? diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 9ee6c0540ea..6f2831670ae 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -44,15 +44,6 @@ pl: remote_follow: Zdalne śledzenie reserved_username: Ta nazwa użytkownika jest zarezerwowana. unfollow: Przestań śledzić - activitypub: - activity: - announce: - name: "%{account_name} udostępnił(a) aktywność." - create: - name: "%{account_name} utworzył(a) wpis." - outbox: - name: Skrzynka %{account_name} - summary: Zbiór aktywności użytkownika %{account_name}. admin: accounts: are_you_sure: Jesteś tego pewien? diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 355c20d05f3..5ba763ae410 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -29,15 +29,6 @@ pt-BR: posts: Posts remote_follow: Acesso remoto unfollow: Unfollow - activitypub: - activity: - announce: - name: "%{account_name} compartilhou uma atividade." - create: - name: "%{account_name} criou uma nota." - outbox: - name: "%{account_name}'s Outbox" - summary: Uma coleção de atividades do usuário %{account_name}. admin: accounts: are_you_sure: Você tem certeza? diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 40be8a6c56d..346fcdda88b 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -29,15 +29,6 @@ pt: posts: Posts remote_follow: Seguir remotamente unfollow: Deixar de seguir - activitypub: - activity: - announce: - name: "%{account_name} anunciou uma atividade." - create: - name: "%{account_name} criou uma nota." - outbox: - name: "%{account_name}'s Outbox" - summary: Uma coleção de atividades do usuário %{account_name}. admin: accounts: are_you_sure: Tens a certeza? diff --git a/config/locales/th.yml b/config/locales/th.yml index 263babdd01c..17eb96110de 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -29,15 +29,6 @@ th: posts: โพสต์ remote_follow: Remote follow unfollow: เลิกติดตาม - activitypub: - activity: - announce: - name: "%{account_name} แชร์กิจกรรม." - create: - name: "%{account_name} สร้างโน๊ต." - outbox: - name: "%{account_name}'s Outbox" - summary: รวมกิจกรรมของผู้ใช้ %{account_name}. admin: accounts: are_you_sure: แน่ใจนะ? diff --git a/config/locales/tr.yml b/config/locales/tr.yml index e7864cc579d..bb83991cd92 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -29,15 +29,6 @@ tr: posts: Gönderiler remote_follow: Uzaktan takip et unfollow: Takibi bırak - activitypub: - activity: - announce: - name: "%{account_name} bir aktivite paylaştı." - create: - name: "%{account_name} bir not oluşturdu." - outbox: - name: "%{account_name}'in Gönderdikleri" - summary: "%{account_name}'den gelen aktiviteler." admin: accounts: are_you_sure: Emin misiniz? diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 650d4bd1599..0526ec1ba1d 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -29,15 +29,6 @@ zh-CN: posts: 嘟文 remote_follow: 跨站关注 unfollow: 取消关注 - activitypub: - activity: - announce: - name: "%{account_name} 分享了一个活动。" - create: - name: "%{account_name} 创建了一个记事。" - outbox: - name: "%{account_name} 的集合" - summary: "%{account_name} 的活动集合" admin: accounts: are_you_sure: 你确定吗? diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index d2db78be162..06f9ab63d48 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -29,15 +29,6 @@ zh-HK: posts: 文章 remote_follow: 跨站關注 unfollow: 取消關注 - activitypub: - activity: - announce: - name: "%{account_name} 分享了一項活動。" - create: - name: "%{account_name} 新增了一篇筆記。" - outbox: - name: "%{account_name} 的活動" - summary: "%{account_name} 分享的活動列表。" admin: accounts: are_you_sure: 你確定嗎? diff --git a/config/routes.rb b/config/routes.rb index 9171d02d4af..dda3534eba6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,7 @@ Rails.application.routes.draw do confirmations: 'auth/confirmations', } - get '/users/:username', to: redirect('/@%{username}'), constraints: { format: :html } + get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? } resources :accounts, path: 'users', only: [:show], param: :username do resources :stream_entries, path: 'updates', only: [:show] do @@ -38,10 +38,17 @@ Rails.application.routes.draw do get :remote_follow, to: 'remote_follow#new' post :remote_follow, to: 'remote_follow#create' + resources :statuses, only: [:show] do + member do + get :activity + end + end + resources :followers, only: [:index], controller: :follower_accounts resources :following, only: [:index], controller: :following_accounts resource :follow, only: [:create], controller: :account_follow resource :unfollow, only: [:create], controller: :account_unfollow + resource :outbox, only: [:show], module: :activitypub end get '/@:username', to: 'accounts#show', as: :short_account @@ -119,13 +126,6 @@ Rails.application.routes.draw do # OEmbed get '/oembed', to: 'oembed#show', as: :oembed - # ActivityPub - namespace :activitypub do - get '/users/:id/outbox', to: 'outbox#show', as: :outbox - get '/statuses/:id', to: 'activities#show_status', as: :status - resources :notes, only: [:show] - end - # JSON / REST API namespace :v1 do resources :statuses, only: [:create, :show, :destroy] do diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 447e2dd53c0..d61c8c9bd35 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -38,7 +38,7 @@ RSpec.describe AccountsController, type: :controller do context 'activitystreams2' do before do - get :show, params: { username: alice.username }, format: 'activitystreams2' + get :show, params: { username: alice.username }, format: 'json' end it 'assigns @account' do diff --git a/spec/controllers/api/activitypub/activities_controller_spec.rb b/spec/controllers/api/activitypub/activities_controller_spec.rb deleted file mode 100644 index 07df28ac2b6..00000000000 --- a/spec/controllers/api/activitypub/activities_controller_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::ActivityPub::ActivitiesController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - - describe 'GET #show' do - describe 'normal status' do - public_status = nil - - before do - public_status = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - - @request.env['HTTP_ACCEPT'] = 'application/activity+json' - get :show_status, params: { id: public_status.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns http success' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('type' => 'Create') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'Create') - expect(json_data).to include('object' => api_activitypub_note_url(public_status)) - expect(json_data).to include('url' => TagManager.instance.url_for(public_status)) - end - end - - describe 'reblog' do - original = nil - reblog = nil - - before do - original = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - reblog = Fabricate(:status, account: user.account, reblog_of_id: original.id, visibility: :public) - - @request.env['HTTP_ACCEPT'] = 'application/activity+json' - get :show_status, params: { id: reblog.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns http success' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('type' => 'Announce') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'Announce') - expect(json_data).to include('object' => api_activitypub_status_url(original)) - expect(json_data).to include('url' => TagManager.instance.url_for(reblog)) - end - end - end -end diff --git a/spec/controllers/api/activitypub/notes_controller_spec.rb b/spec/controllers/api/activitypub/notes_controller_spec.rb deleted file mode 100644 index a0f05dc6593..00000000000 --- a/spec/controllers/api/activitypub/notes_controller_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::ActivityPub::NotesController, type: :controller do - render_views - - let(:user_alice) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:user_bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } - - describe 'GET #show' do - describe 'normal status' do - public_status = nil - - before do - public_status = Fabricate(:status, account: user_alice.account, text: 'Hello world', visibility: :public) - - @request.env['HTTP_ACCEPT'] = 'application/activity+json' - get :show, params: { id: public_status.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns http success' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('type' => 'Note') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('name' => 'Hello world') - expect(json_data).to include('content' => 'Hello world') - expect(json_data).to include('published') - expect(json_data).to include('url' => TagManager.instance.url_for(public_status)) - end - end - - describe 'reply' do - original = nil - reply = nil - - before do - original = Fabricate(:status, account: user_alice.account, text: 'Hello world', visibility: :public) - reply = Fabricate(:status, account: user_bob.account, text: 'Hello world', in_reply_to_id: original.id, visibility: :public) - - @request.env['HTTP_ACCEPT'] = 'application/activity+json' - get :show, params: { id: reply.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns http success' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('type' => 'Note') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('name' => 'Hello world') - expect(json_data).to include('content' => 'Hello world') - expect(json_data).to include('published') - expect(json_data).to include('url' => TagManager.instance.url_for(reply)) - expect(json_data).to include('inReplyTo' => api_activitypub_note_url(original)) - end - end - end -end diff --git a/spec/controllers/api/activitypub/outbox_controller_spec.rb b/spec/controllers/api/activitypub/outbox_controller_spec.rb deleted file mode 100644 index 049cf451d71..00000000000 --- a/spec/controllers/api/activitypub/outbox_controller_spec.rb +++ /dev/null @@ -1,156 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::ActivityPub::OutboxController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - - describe 'GET #show' do - before do - @request.headers['ACCEPT'] = 'application/activity+json' - end - - describe 'collection with small number of statuses' do - public_status = nil - - before do - public_status = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct) - - get :show, params: { id: user.account.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns AS2 JSON body' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'OrderedCollection') - expect(json_data).to include('totalItems' => 1) - expect(json_data).to include('current') - expect(json_data).to include('first') - expect(json_data).to include('last') - end - end - - describe 'collection with large number of statuses' do - before do - 30.times do - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - end - - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct) - - get :show, params: { id: user.account.id } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns AS2 JSON body' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'OrderedCollection') - expect(json_data).to include('totalItems' => 30) - expect(json_data).to include('current') - expect(json_data).to include('first') - expect(json_data).to include('last') - end - end - - describe 'page with small number of statuses' do - statuses = [] - - before do - 5.times do - statuses << Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - end - - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct) - - get :show, params: { id: user.account.id, max_id: statuses.last.id + 1 } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns AS2 JSON body' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'OrderedCollectionPage') - expect(json_data).to include('partOf') - expect(json_data).to include('items') - expect(json_data['items'].length).to eq(5) - expect(json_data).to include('prev') - expect(json_data).to include('next') - expect(json_data).to include('current') - expect(json_data).to include('first') - expect(json_data).to include('last') - end - end - - describe 'page with large number of statuses' do - statuses = [] - - before do - 30.times do - statuses << Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public) - end - - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted) - Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct) - - get :show, params: { id: user.account.id, max_id: statuses.last.id + 1 } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'sets Content-Type header to AS2' do - expect(response.header['Content-Type']).to include 'application/activity+json' - end - - it 'returns AS2 JSON body' do - json_data = JSON.parse(response.body) - expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams') - expect(json_data).to include('id' => @request.url) - expect(json_data).to include('type' => 'OrderedCollectionPage') - expect(json_data).to include('partOf') - expect(json_data).to include('items') - expect(json_data['items'].length).to eq(20) - expect(json_data).to include('prev') - expect(json_data).to include('next') - expect(json_data).to include('current') - expect(json_data).to include('first') - expect(json_data).to include('last') - end - end - end -end diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb index 3699efb5603..466f87c45cd 100644 --- a/spec/controllers/well_known/webfinger_controller_spec.rb +++ b/spec/controllers/well_known/webfinger_controller_spec.rb @@ -9,7 +9,7 @@ describe WellKnown::WebfingerController, type: :controller do end before do - alice.private_key = < - - acct:alice@cb6e6126.ngrok.io - https://cb6e6126.ngrok.io/@alice - https://cb6e6126.ngrok.io/users/alice - - - - - - -XML + expect(xml.at_xpath('//xmlns:Subject').content).to eq 'acct:alice@cb6e6126.ngrok.io' + expect(xml.xpath('//xmlns:Alias').map(&:content)).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice') end it 'returns http not found when account cannot be found' do @@ -80,19 +74,22 @@ XML end it 'returns JSON when account can be found with alternate domains' do - Rails.configuration.x.alternate_domains = ["foo.org"] - username, domain = alice.to_webfinger_s.split("@") + Rails.configuration.x.alternate_domains = ['foo.org'] + username, = alice.to_webfinger_s.split('@') get :show, params: { resource: "#{username}@foo.org" }, format: :json + json = body_as_json + expect(response).to have_http_status(:success) expect(response.content_type).to eq 'application/jrd+json' - expect(response.body).to eq "{\"subject\":\"acct:alice@cb6e6126.ngrok.io\",\"aliases\":[\"https://cb6e6126.ngrok.io/@alice\",\"https://cb6e6126.ngrok.io/users/alice\"],\"links\":[{\"rel\":\"http://webfinger.net/rel/profile-page\",\"type\":\"text/html\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"http://schemas.google.com/g/2010#updates-from\",\"type\":\"application/atom+xml\",\"href\":\"https://cb6e6126.ngrok.io/users/alice.atom\"},{\"rel\":\"self\",\"type\":\"application/activity+json\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"salmon\",\"href\":\"#{api_salmon_url(alice.id)}\"},{\"rel\":\"magic-public-key\",\"href\":\"data:application/magic-public-key,RSA.x4D6DyZa3zGa1XLhd_VG1bLGvK-Dmyz93WJfWNezIKeSuJkmA0f2NmoOfLUoumq9szN2Xt0GLDX06tDajdYPPXgLtDG0o1qqTrIJ7UTyYhbo94Wotl9iJvEwa5IjP1Mn00YJ_KvFrzKCm15PC7up6r-NtHsqoYS8X1KAqcbnptU=.AQAB\"},{\"rel\":\"http://ostatus.org/schema/1.0/subscribe\",\"template\":\"https://cb6e6126.ngrok.io/authorize_follow?acct={uri}\"}]}" + expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io' + expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice') end it 'returns http not found when account can not be found with alternate domains' do - Rails.configuration.x.alternate_domains = ["foo.org"] - username, domain = alice.to_webfinger_s.split("@") + Rails.configuration.x.alternate_domains = ['foo.org'] + username, = alice.to_webfinger_s.split('@') get :show, params: { resource: "#{username}@bar.org" }, format: :json diff --git a/spec/helpers/activitystreams2_builder_helper_spec.rb b/spec/helpers/activitystreams2_builder_helper_spec.rb deleted file mode 100644 index 612ce6ad293..00000000000 --- a/spec/helpers/activitystreams2_builder_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Activitystreams2BuilderHelper, type: :helper do - it 'returns display name if present' do - account = Fabricate(:account, display_name: 'display name', username: 'username') - expect(account_name(account)).to eq 'display name' - end - - it 'returns username if display name is not present' do - account = Fabricate(:account, display_name: '', username: 'username') - expect(account_name(account)).to eq 'username' - end -end From bd915d9398816fd1fb44645eb6d7e2c9df0e6388 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Sat, 15 Jul 2017 19:47:43 +0900 Subject: [PATCH 33/35] Move babel-plugin-preval to dependencies (#4209) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20224796f6f..aede6df2e92 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "babel-core": "^6.25.0", "babel-loader": "^7.1.1", "babel-plugin-lodash": "^3.2.11", + "babel-plugin-preval": "^1.3.2", "babel-plugin-react-intl": "^2.3.1", "babel-plugin-react-transform": "^2.0.2", "babel-plugin-syntax-dynamic-import": "^6.18.0", @@ -121,7 +122,6 @@ "@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", From 05cd37097c134d559550adca6cd17cf7578be94b Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 15 Jul 2017 17:24:35 +0200 Subject: [PATCH 34/35] Optimize uri normalization (#4212) * Add dependency on idn-ruby to speed up URI normalization * Use normalized_host instead of normalize.host when applicable When we are only interested in the normalized host, calling normalized_host avoids normalizing the other components of the URI as well as creating a new object --- Gemfile | 1 + Gemfile.lock | 2 ++ app/lib/tag_manager.rb | 2 +- app/services/concerns/author_extractor.rb | 2 +- app/services/fetch_remote_status_service.rb | 4 ++-- app/workers/pubsubhubbub/delivery_worker.rb | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 5d5ddfae109..a6c2b2d65c6 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'htmlentities', '~> 4.3' gem 'http', '~> 2.2' gem 'http_accept_language', '~> 2.1' gem 'httplog', '~> 0.99' +gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.0' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.1' diff --git a/Gemfile.lock b/Gemfile.lock index daef3e1ad86..f637c9bbe47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,6 +208,7 @@ GEM parser (>= 2.2.3.0) rainbow (~> 2.2) terminal-table (>= 1.5.1) + idn-ruby (0.1.0) jmespath (1.3.1) json (2.1.0) jsonapi-renderer (0.1.2) @@ -528,6 +529,7 @@ DEPENDENCIES http_accept_language (~> 2.1) httplog (~> 0.99) i18n-tasks (~> 0.9) + idn-ruby kaminari (~> 1.0) letter_opener (~> 1.4) letter_opener_web (~> 1.3) diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index f1a2234dc44..5f87a2a48dc 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -70,7 +70,7 @@ class TagManager uri = Addressable::URI.new uri.host = domain.gsub(/[\/]/, '') - uri.normalize.host + uri.normalized_host end def same_acct?(canonical, needle) diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb index 00fe1c66393..867d6dc2597 100644 --- a/app/services/concerns/author_extractor.rb +++ b/app/services/concerns/author_extractor.rb @@ -14,7 +14,7 @@ module AuthorExtractor return nil if username.blank? || uri.blank? - domain = Addressable::URI.parse(uri).normalize.host + domain = Addressable::URI.parse(uri).normalized_host acct = "#{username}@#{domain}" end diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 4cfd33d90a0..6ac31e4d8b5 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -24,7 +24,7 @@ class FetchRemoteStatusService < BaseService xml.encoding = 'utf-8' account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)) - domain = Addressable::URI.parse(url).normalize.host + domain = Addressable::URI.parse(url).normalized_host return nil unless !account.nil? && confirmed_domain?(domain, account) @@ -39,6 +39,6 @@ class FetchRemoteStatusService < BaseService end def confirmed_domain?(domain, account) - account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url).normalize.host).zero? + account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url).normalized_host).zero? end end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 05d160cf7c7..2e1101b93cc 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -43,7 +43,7 @@ class Pubsubhubbub::DeliveryWorker end def host - Addressable::URI.parse(subscription.callback_url).normalize.host + Addressable::URI.parse(subscription.callback_url).normalized_host end def headers From 695439775eacea081c7257aabab39d0ec6b492dc Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 16 Jul 2017 00:25:04 +0900 Subject: [PATCH 35/35] Fix column swiping (#4211) This fixes broken behavior and enable animation only on swiping. --- .../features/ui/components/columns_area.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index ae3f9261b8c..515c377b9d4 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 ? (