From 126f929c3961b3f897ba9fa35890af27d0421961 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Tue, 27 Jun 2017 23:10:43 +0200 Subject: i18n: Use instance name in email notifications instead of Mastodon (pl) (#3976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- app/views/user_mailer/password_change.pl.html.erb | 2 +- app/views/user_mailer/password_change.pl.text.erb | 2 +- app/views/user_mailer/reset_password_instructions.pl.html.erb | 3 ++- app/views/user_mailer/reset_password_instructions.pl.text.erb | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) (limited to 'app/views') diff --git a/app/views/user_mailer/password_change.pl.html.erb b/app/views/user_mailer/password_change.pl.html.erb index 46423483a..a7cb15a05 100644 --- a/app/views/user_mailer/password_change.pl.html.erb +++ b/app/views/user_mailer/password_change.pl.html.erb @@ -1,3 +1,3 @@

Witaj, <%= @resource.email %>!

-

Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.

+

Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.

diff --git a/app/views/user_mailer/password_change.pl.text.erb b/app/views/user_mailer/password_change.pl.text.erb index 85d5e1175..bd2efee0f 100644 --- a/app/views/user_mailer/password_change.pl.text.erb +++ b/app/views/user_mailer/password_change.pl.text.erb @@ -1,3 +1,3 @@ Witaj, <%= @resource.email %>! -Informujemy, że ostatnio zmieniono Twoje hasło Mastodona. +Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>. diff --git a/app/views/user_mailer/reset_password_instructions.pl.html.erb b/app/views/user_mailer/reset_password_instructions.pl.html.erb index f4d67c724..2a9913a1d 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.html.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.html.erb @@ -1,6 +1,7 @@

Witaj, <%= @resource.email %>!

-

Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.

+

Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link.

<%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %>

diff --git a/app/views/user_mailer/reset_password_instructions.pl.text.erb b/app/views/user_mailer/reset_password_instructions.pl.text.erb index 78d1cab0b..2b34afc48 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.text.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.text.erb @@ -1,6 +1,7 @@ Witaj, <%= @resource.email %>! -Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link. +Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link. <%= edit_password_url(@resource, reset_password_token: @token) %> -- cgit From 92f1c474f305de2383a4d2b3dffbcc20800cfb6f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 3 Jul 2017 18:04:35 +0900 Subject: Add fa-fw class to user agent icon (#4047) --- app/helpers/application_helper.rb | 8 ++++++-- app/views/auth/registrations/_sessions.html.haml | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'app/views') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 36c37fae0..9f50d8bdb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -31,7 +31,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end - def fa_icon(icon) - content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' ')) + def fa_icon(icon, attributes = {}) + class_names = attributes[:class]&.split(' ') || [] + class_names << 'fa' + class_names += icon.split(' ').map { |cl| "fa-#{cl}" } + + content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end end diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 11c0d4e31..4521aad0a 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -11,9 +11,10 @@ - @sessions.each do |session| %tr %td - %span{ title: session.user_agent }= fa_icon session_device_icon(session) - = ' ' - = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") + %span{ title: session.user_agent }< + = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session) + = ' ' + = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") %td %samp= session.ip %td -- cgit From 275c5b51ed7e22734d18db6acb2b87ba26bd435f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 4 Jul 2017 22:19:24 +0900 Subject: Customizable privacy policy from admin interface (#4062) --- app/controllers/about_controller.rb | 2 +- app/controllers/admin/settings_controller.rb | 1 + app/presenters/instance_presenter.rb | 1 + app/views/about/terms.en.html.haml | 76 ---------------------------- app/views/about/terms.html.haml | 8 +++ app/views/about/terms.ja.html.haml | 76 ---------------------------- app/views/about/terms.no.html.haml | 76 ---------------------------- app/views/admin/settings/edit.html.haml | 7 +++ config/locales/en.yml | 73 ++++++++++++++++++++++++++ config/locales/ja.yml | 70 +++++++++++++++++++++++++ config/locales/no.yml | 70 +++++++++++++++++++++++++ config/settings.yml | 1 + 12 files changed, 232 insertions(+), 229 deletions(-) delete mode 100644 app/views/about/terms.en.html.haml create mode 100644 app/views/about/terms.html.haml delete mode 100644 app/views/about/terms.ja.html.haml delete mode 100644 app/views/about/terms.no.html.haml (limited to 'app/views') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 04e7ddacf..c0addbecc 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,7 +2,7 @@ class AboutController < ApplicationController before_action :set_body_classes - before_action :set_instance_presenter, only: [:show, :more] + before_action :set_instance_presenter, only: [:show, :more, :terms] def show; end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index fcd42c79c..7542f55e8 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -8,6 +8,7 @@ module Admin site_title site_description site_extended_description + site_terms open_registrations closed_registrations_message ).freeze diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 9a69809d0..63ef23d5d 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -7,6 +7,7 @@ class InstancePresenter :open_registrations, :site_description, :site_extended_description, + :site_terms, to: Setting ) diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml deleted file mode 100644 index 7e0fb94c2..000000000 --- a/app/views/about/terms.en.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Terms of Service and Privacy Policy - -.wrapper - %h2 Privacy Policy - - %h3#collect What information do we collect? - - %p We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here. - - %p When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address. - - %p When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server. - - %h3#use What do we use your information for? - - %p Any of the information we collect from you may be used in one of the following ways: - - %ul - %li To personalize your experience — your information helps us to better respond to your individual needs. - %li To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you. - %li To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs. - %li To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions. - - %h3#protect How do we protect your information? - - %p We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. - - %h3#data-retention What is your data retention policy? - - %p We will make a good faith effort to: - - %ul - %li Retain server logs containing the IP address of all requests to this server no more than 90 days. - %li Retain the IP addresses associated with registered users and their posts no more than 5 years. - - %h3#cookies Do we use cookies? - - %p Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. - - %p We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business. - - %h3#disclose Do we disclose any information to outside parties? - - %p We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses. - - %h3#third-party Third party links - - %p Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites. - - %h3#coppa Children's Online Privacy Protection Act Compliance - - %p - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - do not use this site. - - %h3#online Online Privacy Policy Only - - %p This online privacy policy applies only to information collected through our site and not to information collected offline. - - %h3#consent Your Consent - - %p By using our site, you consent to our web site privacy policy. - - %h3#changes Changes to our Privacy Policy - - %p If we decide to change our privacy policy, we will post those changes on this page. - - %p This document is CC-BY-SA. It was last updated May 31, 2013. - - %p - Originally adapted from the - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml new file mode 100644 index 000000000..58064f0be --- /dev/null +++ b/app/views/about/terms.html.haml @@ -0,0 +1,8 @@ +- content_for :page_title do + = t('terms.title', instance: site_hostname) + +.wrapper + - if @instance_presenter.site_terms.present? + = raw @instance_presenter.site_terms + - else + = t('terms.body_html') diff --git a/app/views/about/terms.ja.html.haml b/app/views/about/terms.ja.html.haml deleted file mode 100644 index 5c546b3e0..000000000 --- a/app/views/about/terms.ja.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} 利用規約・プライバシーポリシー - -.wrapper - %h2 プライバシーポリシー - - %h3#collect どのような情報を収集するのですか? - - %p あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。 - - %p このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。 - - %p アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。 - - %h3#use 自分の情報を何に使うのですか? - - %p このサイトで収集された情報は、次のいくつかの方法で使用されます: - - %ul - %li パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。 - %li サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。 - %li サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。 - %li 定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。 - - %h3#protect 自分の情報はどのように保護されるのですか? - - %p このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。 - - %h3#data-retention データ保持のポリシーはどのようになっていますか? - - %p このサービスはデータ保持に関して次のことを行うよう努めます。: - - %ul - %li このサーバーへのすべての要求に対して、IPアドレスを含むサーバーログを90日以内に渡って保持します。 - %li 登録されたユーザーとその投稿に関連付けされたIPアドレスを5年以内に渡って保持します。 - - %h3#cookies クッキーを使用していますか? - - %p はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。 - - %p クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。 - - %h3#disclose このサイトは外部に何らかの情報を開示していますか? - - %p 私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。 - - %h3#third-party サードパーティのリンク - - %p 必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。 - - %h3#coppa 子供のオンライン・プライバシー保護法 - - %p - このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - にもとづいてこのサイトを使用しないでください。 - - %h3#online オンライン限定のプライバシーポリシー - - %p このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。 - - %h3#consent あなたの同意 - - %p このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。 - - %h3#changes プライバシーポリシーの変更 - - %p プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。 - - %p この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。 - - %p - オリジナルの出典 - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml deleted file mode 100644 index 46f62950d..000000000 --- a/app/views/about/terms.no.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Personvern og villkår for bruk av nettstedet - -.wrapper - %h2 Personvernserklæring - - %h3#collect Hvilke opplysninger samler vi? - - %p Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her. - - %p Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen. - - %p Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår. - - %h3#use Hva bruker vi opplysningene dine til? - - %p Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter: - - %ul - %li For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov. - %li For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg. - %li For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte. - %li For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål. - - %h3#protect Hvordan sikrer vi opplysningene? - - %p Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem. - - %h3#data-retention Hva er retningslinjene deres for lagring av data? - - %p Vi vil forsøke i god tro å: - - %ul - %li Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager. - %li Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år. - - %h3#cookies Bruker vi informasjonskapsler? - - %p Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den. - - %p Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt. - - %h3#disclose Gir vi noen opplysninger videre til andre parter? - - %p Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk. - - %h3#third-party Tredjeparts lenker - - %p Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne. - - %h3#coppa Overensstemmelse med Children's Online Privacy Protection Act - - %p - Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - ikke bruk dette nettstedet. - - %h3#online Personvernerklæring bare for nettet - - %p Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet. - - %h3#consent Ditt samtykke - - %p Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring. - - %h3#changes Endringer i vår personvernerklæring - - %p Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden. - - %p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017. - - %p - Dokumentet er en adoptert og endret versjon fra - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index edb69e360..3096a958d 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -40,6 +40,13 @@ %td= text_area_tag :site_extended_description, @settings['site_extended_description'].value, rows: 8 + %tr + %td + %strong= t('admin.settings.site_terms.title') + %p= t('admin.settings.site_terms.desc_html') + %td= text_area_tag :site_terms, + @settings['site_terms'].value, + rows: 8 %tr %td %strong= t('admin.settings.registrations.open.title') diff --git a/config/locales/en.yml b/config/locales/en.yml index 944c24c60..60e192491 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -183,6 +183,9 @@ en: site_description_extended: desc_html: Displayed on extended information page
You can use HTML tags title: Extended site description + site_terms: + desc_html: Displayed on terms page
You can use HTML tags + title: Site Privacy Policy site_title: Site title title: Site Settings subscriptions: @@ -387,6 +390,76 @@ en: click_to_show: Click to show reblogged: boosted sensitive_content: Sensitive content + terms: + body_html: | +

Privacy Policy

+ +

What information do we collect?

+ +

We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here.

+ +

When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address.

+ +

When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server.

+ +

What do we use your information for?

+ +

Any of the information we collect from you may be used in one of the following ways:

+ +
    +
  • To personalize your experience — your information helps us to better respond to your individual needs.
  • +
  • To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you.
  • +
  • To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs.
  • +
  • To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions.
  • +
+ +

How do we protect your information?

+ +

We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information.

+ +

What is your data retention policy?

+ +

We will make a good faith effort to:

+ +
    +
  • Retain server logs containing the IP address of all requests to this server no more than 90 days.
  • +
  • Retain the IP addresses associated with registered users and their posts no more than 5 years.
  • +
+ +

Do we use cookies?

+ +

Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.

+ +

We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business.

+ +

Do we disclose any information to outside parties?

+ +

We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.

+ +

Third party links

+ +

Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites.

+ +

Children's Online Privacy Protection Act Compliance

+ +

Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.

+ +

Online Privacy Policy Only

+ +

This online privacy policy applies only to information collected through our site and not to information collected offline.

+ + + +

By using our site, you consent to our web site privacy policy.

+ +

Changes to our Privacy Policy

+ +

If we decide to change our privacy policy, we will post those changes on this page.

+ +

This document is CC-BY-SA. It was last updated May 31, 2013.

+ +

Originally adapted from the Discourse privacy policy. + title: "%{instance} Terms of Service and Privacy Policy" time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b45e06d3a..347270af2 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -387,6 +387,76 @@ ja: click_to_show: クリックして表示 reblogged: さんにブーストされました sensitive_content: 閲覧注意 + terms: + body_html: | +

プライバシーポリシー

+ +

どのような情報を収集するのですか?

+ +

あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。

+ +

このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。

+ +

アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。

+ +

自分の情報を何に使うのですか?

+ +

このサイトで収集された情報は、次のいくつかの方法で使用されます:

+ +
    +
  • パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。
  • +
  • サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。
  • +
  • サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。
  • +
  • 定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。
  • +
+ +

自分の情報はどのように保護されるのですか?

+ +

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

+ +

クッキーを使用していますか?

+ +

はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。

+ +

クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。

+ +

このサイトは外部に何らかの情報を開示していますか?

+ +

私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。

+ +

サードパーティのリンク

+ +

必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。

+ +

子供のオンライン・プライバシー保護法

+ +

このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA (Children's Online Privacy Protection Act) にもとづいてこのサイトを使用しないでください。

+ +

オンライン限定のプライバシーポリシー

+ +

このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。

+ + + +

このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。

+ +

プライバシーポリシーの変更

+ +

プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。

+ +

この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。

+ +

オリジナルの出典 Discourse privacy policy. + title: "%{instance} 利用規約・プライバシーポリシー" time: formats: default: "%Y年%m月%d日 %H:%M" diff --git a/config/locales/no.yml b/config/locales/no.yml index f71c08c6a..5fd63f121 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -321,6 +321,76 @@ click_to_show: Klikk for å vise reblogged: fremhevde sensitive_content: Følsomt innhold + terms: + body_html: | +

Personvernserklæring

+ +

Hvilke opplysninger samler vi?

+ +

Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her.

+ +

Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen.

+ +

Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår.

+ +

Hva bruker vi opplysningene dine til?

+ +

Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter:

+ +
    +
  • For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov.
  • +
  • For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg.
  • +
  • For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte.
  • +
  • For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål.
  • +
+ +

Hvordan sikrer vi opplysningene?

+ +

Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem.

+ +

Hva er retningslinjene deres for lagring av data?

+ +

Vi vil forsøke i god tro å:

+ +
    +
  • Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager.
  • +
  • Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år.
  • +
+ +

Bruker vi informasjonskapsler?

+ +

Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den.

+ +

Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt.

+ +

Gir vi noen opplysninger videre til andre parter?

+ +

Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk.

+ +

Tredjeparts lenker

+ +

Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne.

+ +

Overensstemmelse med Children's Online Privacy Protection Act

+ +

Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA (Children's Online Privacy Protection Act), ikke bruk dette nettstedet.

+ +

Personvernerklæring bare for nettet

+ +

Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet.

+ + + +

Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring.

+ +

Endringer i vår personvernerklæring

+ +

Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden.

+ +

Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.

+ +

Dokumentet er en adoptert og endret versjon fra Discourse privacy policy.

+ title: "%{instance} Personvern og villkår for bruk av nettstedet" time: formats: default: "%-d. %b %Y, %H:%M" diff --git a/config/settings.yml b/config/settings.yml index 7b78b6cdb..5aea232e7 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,6 +10,7 @@ defaults: &defaults site_title: Mastodon site_description: '' site_extended_description: '' + site_terms: '' site_contact_username: '' site_contact_email: '' open_registrations: true -- cgit From bb194ddb3c65394c256c381834ba3bce4d2988cc Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 5 Jul 2017 21:51:28 +0900 Subject: Format datetime of subscriptions on admin UI (#4078) --- app/views/admin/subscriptions/_subscription.html.haml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'app/views') diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml index 024788e13..1dec8e396 100644 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ b/app/views/admin/subscriptions/_subscription.html.haml @@ -7,10 +7,12 @@ - if subscription.confirmed? %i.fa.fa-check %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) + %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } + = precede subscription.expired? ? '-' : '' do + = time_ago_in_words(subscription.expires_at) %td - if subscription.last_successful_delivery_at? - = l subscription.last_successful_delivery_at + %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } + = l subscription.last_successful_delivery_at - else %i.fa.fa-times -- cgit From f76e71825da3b185e549cd3267813fd12cee4050 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 7 Jul 2017 04:31:03 +0900 Subject: Improve Activity stream spoiler (#4088) --- app/javascript/packs/public.js | 8 ++++++-- app/javascript/styles/stream_entries.scss | 12 ++++++++++++ app/views/stream_entries/_content_spoiler.html.haml | 10 +++++++--- app/views/stream_entries/_detailed_status.html.haml | 6 ++---- app/views/stream_entries/_simple_status.html.haml | 3 +-- 5 files changed, 28 insertions(+), 11 deletions(-) (limited to 'app/views') diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index a0e511b0a..254250a3b 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -53,8 +53,12 @@ function main() { } }); - delegate(document, '.media-spoiler', 'click', ({ target }) => { - target.style.display = 'none'; + delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { + this.parentNode.classList.add('media-spoiler-wrapper__visible'); + }); + + delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { + this.parentNode.classList.remove('media-spoiler-wrapper__visible'); }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index fcec32d44..e89cc3f09 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -330,6 +330,18 @@ } } + .media-spoiler-wrapper { + &.media-spoiler-wrapper__visible { + .media-spoiler { + display: none; + } + + .spoiler-button { + display: block; + } + } + } + .pre-header { padding: 14px 0; padding-left: (48px + 14px * 2); diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml index d80ea46a0..798dfce67 100644 --- a/app/views/stream_entries/_content_spoiler.html.haml +++ b/app/views/stream_entries/_content_spoiler.html.haml @@ -1,3 +1,7 @@ -.media-spoiler - %span= t('stream_entries.sensitive_content') - %span= t('stream_entries.click_to_show') +.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' } + .spoiler-button + .icon-button.overlayed + %i.fa.fa-fw.fa-eye + .media-spoiler + %span= t('stream_entries.sensitive_content') + %span= t('stream_entries.click_to_show') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index ef60b9925..193cc6470 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -17,13 +17,11 @@ - unless status.media_attachments.empty? - if status.media_attachments.first.video? .video-player - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } %video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true } - else .detailed-status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } .status__attachments__inner - status.media_attachments.each do |media| = render partial: 'stream_entries/media', locals: { media: media } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index db4e30fda..2df0cc850 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,8 +22,7 @@ - unless status.media_attachments.empty? .status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } - if status.media_attachments.first.video? .status__attachments__inner .video-item -- cgit From 18d3fa953b5af8ab17cc93c33cb95cec37127712 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Thu, 6 Jul 2017 13:39:56 -0700 Subject: Add a setting allowing the use of system's default font in Web UI (#4033) * add a system_font_ui setting on the server * Plug the system_font_ui on the front-end * add EN/FR locales for the new setting * put Roboto after all other fonts * remove trailing whitespace so CodeClimate is happy * fix user_spec.rb * correctly write user_spect this time * slightly better way of adding the classes * add comments to the system-font stack for clarification * use .system-font for the class instead * don't use multiple lines for comments * remove trailing whitespace * use the classnames module for consistency * use `mastodon-font-sans-serif` instead of Roboto directly --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/features/ui/index.js | 14 ++++++++++++-- app/javascript/styles/basics.scss | 15 +++++++++++++++ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/views/home/initial_state.json.rabl | 1 + app/views/settings/preferences/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/locales/simple_form.fr.yml | 1 + config/settings.yml | 1 + spec/lib/user_settings_decorator_spec.rb | 7 +++++++ spec/models/user_spec.rb | 8 ++++++++ 12 files changed, 57 insertions(+), 2 deletions(-) (limited to 'app/views') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 71f5a7c04..a15c26031 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -37,6 +37,7 @@ class Settings::PreferencesController < ApplicationController :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_system_font_ui, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 08d087da1..54e623d99 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import Switch from 'react-router-dom/Switch'; import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; @@ -72,12 +73,17 @@ class WrappedRoute extends React.Component { } -@connect() +const mapStateToProps = state => ({ + systemFontUi: state.getIn(['meta', 'system_font_ui']), +}); + +@connect(mapStateToProps) export default class UI extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, + systemFontUi: PropTypes.bool, }; state = { @@ -176,8 +182,12 @@ export default class UI extends React.PureComponent { const { width, draggingOver } = this.state; const { children } = this.props; + const className = classNames('ui', { + 'system-font': this.props.systemFontUi, + }); + return ( -
+
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 0cb271ddd..4da698e81 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -63,3 +63,18 @@ button { align-items: center; justify-content: center; } + +.system-font { + // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+) + // -apple-system => Safari <11 specific + // BlinkMacSystemFont => Chrome <56 on macOS specific + // Segoe UI => Windows 7/8/10 + // Oxygen => KDE + // Ubuntu => Unity/Ubuntu + // Cantarell => GNOME + // Fira Sans => Firefox OS + // Droid Sans => Older Androids (<4.0) + // Helvetica Neue => Older macOS <10.11 + // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0) + font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif; +} diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index af264bbd5..9c0cb4545 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -21,6 +21,7 @@ class UserSettingsDecorator user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference + user.settings['system_font_ui'] = system_font_ui_preference end def merged_notification_emails @@ -43,6 +44,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_delete_modal' end + def system_font_ui_preference + boolean_cast_setting 'setting_system_font_ui' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/models/user.rb b/app/models/user.rb index c31a0c644..e2bb3d0ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -91,6 +91,10 @@ class User < ApplicationRecord settings.auto_play_gif end + def setting_system_font_ui + settings.system_font_ui + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index e305f8e7a..291ff806b 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -11,6 +11,7 @@ node(:meta) do boost_modal: current_account.user.setting_boost_modal, delete_modal: current_account.user.setting_delete_modal, auto_play_gif: current_account.user.setting_auto_play_gif, + system_font_ui: current_account.user.setting_system_font_ui, } end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 721ce6a21..26fbfdf82 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -44,6 +44,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 3e769fb96..d8d3b8a6f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,6 +38,7 @@ en: setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy setting_delete_modal: Show confirmation dialog before deleting a toot + setting_system_font_ui: Use system's default font severity: Severity type: Import type username: Username diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index ae4975143..446c56947 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -28,6 +28,7 @@ fr: password: Mot de passe setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité type: Type d'import username: Identifiant diff --git a/config/settings.yml b/config/settings.yml index 5aea232e7..18b70b51f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -19,6 +19,7 @@ defaults: &defaults boost_modal: false auto_play_gif: true delete_modal: true + system_font_ui: false notification_emails: follow: false reblog: false diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index 66e42fa0e..e1ba56d97 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -48,5 +48,12 @@ describe UserSettingsDecorator do settings.update(values) expect(user.settings['auto_play_gif']).to eq false end + + it 'updates the user settings value for system font in UI' do + values = { 'setting_system_font_ui' => '0' } + + settings.update(values) + expect(user.settings['system_font_ui']).to eq false + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a6df3fb26..2019ec0f6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -184,6 +184,14 @@ RSpec.describe User, type: :model do expect(user.setting_auto_play_gif).to eq false end end + + describe '#setting_system_font_ui' do + it 'returns system font ui setting' do + user = Fabricate(:user) + user.settings[:system_font_ui] = false + expect(user.setting_system_font_ui).to eq false + end + end describe '#setting_boost_modal' do it 'returns boost modal setting' do -- cgit From 2d6128672fcadeb29c99551a33648b4880969d22 Mon Sep 17 00:00:00 2001 From: のら Date: Fri, 7 Jul 2017 07:48:09 +0900 Subject: Togglable filter links (#4021) * Togglable filter links * Rename is_selected to selected? --- app/helpers/admin/filter_helper.rb | 12 +++++++++--- app/views/admin/accounts/index.html.haml | 24 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) (limited to 'app/views') diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 0dfa30e56..6a57b3d63 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -6,15 +6,21 @@ module Admin::FilterHelper FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS - def filter_link_to(text, more_params) - new_url = filtered_url_for(more_params) - link_to text, new_url, class: filter_link_class(new_url) + def filter_link_to(text, link_to_params, link_class_params = link_to_params) + new_url = filtered_url_for(link_to_params) + new_class = filtered_url_for(link_class_params) + link_to text, new_url, class: filter_link_class(new_class) end def table_link_to(icon, text, path, options = {}) link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') end + def selected?(more_params) + new_url = filtered_url_for(more_params) + filter_link_class(new_url) == 'selected' ? true : false + end + private def filter_params(more_params) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 6d2849c32..07c8d1632 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -6,14 +6,30 @@ %strong= t('admin.accounts.location.title') %ul %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil - %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil - %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil + %li + - if selected? local: '1', remote: nil + = filter_link_to t('admin.accounts.location.local'), {local: nil, remote: nil}, {local: '1', remote: nil} + - else + = filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil + %li + - if selected? remote: '1', local: nil + = filter_link_to t('admin.accounts.location.remote'), {remote: nil, local: nil}, {remote: '1', local: nil} + - else + = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil .filter-subset %strong= t('admin.accounts.moderation.title') %ul %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil - %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' - %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' + %li + - if selected? silenced: '1' + = filter_link_to t('admin.accounts.moderation.silenced'), {silenced: nil}, {silenced: '1'} + - else + = filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' + %li + - if selected? suspended: '1' + = filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'} + - else + = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' .filter-subset %strong= t('admin.accounts.order.title') %ul -- cgit From 8b2cad56374b2dbb6e7a445e7917810935c45536 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 04:02:06 +0200 Subject: Refactor JSON templates to be generated with ActiveModelSerializers instead of Rabl (#4090) --- Gemfile | 1 + Gemfile.lock | 9 +++ .../api/v1/accounts/credentials_controller.rb | 4 +- .../v1/accounts/follower_accounts_controller.rb | 2 +- .../v1/accounts/following_accounts_controller.rb | 2 +- .../api/v1/accounts/relationships_controller.rb | 11 ++- .../api/v1/accounts/search_controller.rb | 3 +- .../api/v1/accounts/statuses_controller.rb | 5 +- app/controllers/api/v1/accounts_controller.rb | 38 +++------ app/controllers/api/v1/apps_controller.rb | 1 + app/controllers/api/v1/blocks_controller.rb | 1 + app/controllers/api/v1/favourites_controller.rb | 5 +- .../api/v1/follow_requests_controller.rb | 1 + app/controllers/api/v1/follows_controller.rb | 2 +- app/controllers/api/v1/instances_controller.rb | 4 +- app/controllers/api/v1/media_controller.rb | 1 + app/controllers/api/v1/mutes_controller.rb | 1 + app/controllers/api/v1/notifications_controller.rb | 7 +- app/controllers/api/v1/reports_controller.rb | 3 +- app/controllers/api/v1/search_controller.rb | 3 +- .../statuses/favourited_by_accounts_controller.rb | 2 +- .../api/v1/statuses/favourites_controller.rb | 4 +- .../api/v1/statuses/mutes_controller.rb | 4 +- .../statuses/reblogged_by_accounts_controller.rb | 2 +- .../api/v1/statuses/reblogs_controller.rb | 4 +- app/controllers/api/v1/statuses_controller.rb | 16 ++-- .../api/v1/timelines/home_controller.rb | 6 +- .../api/v1/timelines/public_controller.rb | 6 +- app/controllers/api/v1/timelines/tag_controller.rb | 6 +- app/lib/inline_rabl_scope.rb | 17 ---- app/lib/inline_renderer.rb | 36 +++++++-- app/models/context.rb | 5 ++ app/models/search.rb | 5 ++ app/presenters/account_relationships_presenter.rb | 15 ++++ app/presenters/status_relationships_presenter.rb | 19 +++++ app/serializers/rest/account_serializer.rb | 33 ++++++++ app/serializers/rest/application_serializer.rb | 14 ++++ app/serializers/rest/context_serializer.rb | 6 ++ app/serializers/rest/instance_serializer.rb | 30 +++++++ .../rest/media_attachment_serializer.rb | 24 ++++++ app/serializers/rest/notification_serializer.rb | 12 +++ app/serializers/rest/preview_card_serializer.rb | 14 ++++ app/serializers/rest/relationship_serializer.rb | 30 +++++++ app/serializers/rest/report_serializer.rb | 5 ++ app/serializers/rest/search_serializer.rb | 12 +++ app/serializers/rest/status_serializer.rb | 93 ++++++++++++++++++++++ app/services/fan_out_on_write_service.rb | 2 +- app/services/notify_service.rb | 2 +- app/views/api/v1/accounts/index.rabl | 2 - app/views/api/v1/accounts/relationship.rabl | 9 --- app/views/api/v1/accounts/relationships/index.rabl | 2 - app/views/api/v1/accounts/show.rabl | 12 --- app/views/api/v1/accounts/statuses/index.rabl | 2 - app/views/api/v1/apps/create.rabl | 4 - app/views/api/v1/apps/show.rabl | 3 - app/views/api/v1/blocks/index.rabl | 2 - app/views/api/v1/favourites/index.rabl | 2 - app/views/api/v1/follow_requests/index.rabl | 2 - app/views/api/v1/follows/show.rabl | 2 - app/views/api/v1/instances/show.rabl | 10 --- app/views/api/v1/media/create.rabl | 7 -- app/views/api/v1/mutes/index.rabl | 2 - app/views/api/v1/notifications/index.rabl | 2 - app/views/api/v1/notifications/show.rabl | 11 --- app/views/api/v1/reports/index.rabl | 2 - app/views/api/v1/reports/show.rabl | 2 - app/views/api/v1/search/index.rabl | 13 --- app/views/api/v1/statuses/_media.rabl | 6 -- app/views/api/v1/statuses/_mention.rabl | 4 - app/views/api/v1/statuses/_show.rabl | 29 ------- app/views/api/v1/statuses/_tags.rabl | 2 - app/views/api/v1/statuses/accounts.rabl | 2 - app/views/api/v1/statuses/card.rabl | 7 -- app/views/api/v1/statuses/context.rabl | 9 --- app/views/api/v1/statuses/index.rabl | 2 - app/views/api/v1/statuses/show.rabl | 15 ---- app/views/api/v1/timelines/show.rabl | 2 - app/views/home/initial_state.json.rabl | 4 +- app/workers/push_update_worker.rb | 2 +- spec/lib/inline_rabl_scope_spec.rb | 23 ------ 80 files changed, 425 insertions(+), 301 deletions(-) delete mode 100644 app/lib/inline_rabl_scope.rb create mode 100644 app/models/context.rb create mode 100644 app/models/search.rb create mode 100644 app/presenters/account_relationships_presenter.rb create mode 100644 app/presenters/status_relationships_presenter.rb create mode 100644 app/serializers/rest/account_serializer.rb create mode 100644 app/serializers/rest/application_serializer.rb create mode 100644 app/serializers/rest/context_serializer.rb create mode 100644 app/serializers/rest/instance_serializer.rb create mode 100644 app/serializers/rest/media_attachment_serializer.rb create mode 100644 app/serializers/rest/notification_serializer.rb create mode 100644 app/serializers/rest/preview_card_serializer.rb create mode 100644 app/serializers/rest/relationship_serializer.rb create mode 100644 app/serializers/rest/report_serializer.rb create mode 100644 app/serializers/rest/search_serializer.rb create mode 100644 app/serializers/rest/status_serializer.rb delete mode 100644 app/views/api/v1/accounts/index.rabl delete mode 100644 app/views/api/v1/accounts/relationship.rabl delete mode 100644 app/views/api/v1/accounts/relationships/index.rabl delete mode 100644 app/views/api/v1/accounts/show.rabl delete mode 100644 app/views/api/v1/accounts/statuses/index.rabl delete mode 100644 app/views/api/v1/apps/create.rabl delete mode 100644 app/views/api/v1/apps/show.rabl delete mode 100644 app/views/api/v1/blocks/index.rabl delete mode 100644 app/views/api/v1/favourites/index.rabl delete mode 100644 app/views/api/v1/follow_requests/index.rabl delete mode 100644 app/views/api/v1/follows/show.rabl delete mode 100644 app/views/api/v1/instances/show.rabl delete mode 100644 app/views/api/v1/media/create.rabl delete mode 100644 app/views/api/v1/mutes/index.rabl delete mode 100644 app/views/api/v1/notifications/index.rabl delete mode 100644 app/views/api/v1/notifications/show.rabl delete mode 100644 app/views/api/v1/reports/index.rabl delete mode 100644 app/views/api/v1/reports/show.rabl delete mode 100644 app/views/api/v1/search/index.rabl delete mode 100644 app/views/api/v1/statuses/_media.rabl delete mode 100644 app/views/api/v1/statuses/_mention.rabl delete mode 100644 app/views/api/v1/statuses/_show.rabl delete mode 100644 app/views/api/v1/statuses/_tags.rabl delete mode 100644 app/views/api/v1/statuses/accounts.rabl delete mode 100644 app/views/api/v1/statuses/card.rabl delete mode 100644 app/views/api/v1/statuses/context.rabl delete mode 100644 app/views/api/v1/statuses/index.rabl delete mode 100644 app/views/api/v1/statuses/show.rabl delete mode 100644 app/views/api/v1/timelines/show.rabl delete mode 100644 spec/lib/inline_rabl_scope_spec.rb (limited to 'app/views') diff --git a/Gemfile b/Gemfile index 6ee884a17..95c74eef9 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ gem 'aws-sdk', '~> 2.9' gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder', '~> 0.6' +gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.5' gem 'bootsnap' gem 'browser' diff --git a/Gemfile.lock b/Gemfile.lock index f0156529c..71f83f736 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,6 +24,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) + active_model_serializers (0.10.6) + actionpack (>= 4.1, < 6) + activemodel (>= 4.1, < 6) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.2) active_record_query_trace (1.5.4) activejob (5.1.2) activesupport (= 5.1.2) @@ -101,6 +106,8 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + case_transform (0.2) + activesupport chunky_png (1.3.8) cld3 (3.1.3) ffi (>= 1.1.0, < 1.10.0) @@ -200,6 +207,7 @@ GEM terminal-table (>= 1.5.1) jmespath (1.3.1) json (2.1.0) + jsonapi-renderer (0.1.2) kaminari (1.0.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.0.1) @@ -476,6 +484,7 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.10) active_record_query_trace (~> 1.5) addressable (~> 2.5) annotate (~> 2.7) diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 1cf52ff10..8ee9a2416 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def show @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::AccountSerializer end def update current_account.update!(account_params) @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 81aae56d3..80b0bef40 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 63c6d54b2..55cffdf37 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index cb923ab91..a88cf2021 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -8,16 +8,15 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController def index @accounts = Account.where(id: account_ids).select('id') - @following = Account.following_map(account_ids, current_user.account_id) - @followed_by = Account.followed_by_map(account_ids, current_user.account_id) - @blocking = Account.blocking_map(account_ids, current_user.account_id) - @muting = Account.muting_map(account_ids, current_user.account_id) - @requested = Account.requested_map(account_ids, current_user.account_id) - @domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id) + render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end private + def relationships + AccountRelationshipsPresenter.new(@accounts, current_user.account_id) + end + def account_ids @_account_ids ||= Array(params[:id]).map(&:to_i) end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index c4a8f97f2..2a5cac547 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -8,8 +8,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController def show @accounts = account_search - - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 504ed8c07..d9ae5c089 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -9,6 +9,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses.tap do |statuses| - set_maps(statuses) - end + cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 8fc0dd36f..f621aa245 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -8,49 +8,38 @@ class Api::V1::AccountsController < Api::BaseController respond_to :json - def show; end + def show + render json: @account, serializer: REST::AccountSerializer + end def follow FollowService.new.call(current_user.account, @account.acct) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def block BlockService.new.call(current_user.account, @account) - - @following = { @account.id => false } - @followed_by = { @account.id => false } - @blocking = { @account.id => true } - @requested = { @account.id => false } - @muting = { @account.id => current_account.muting?(@account.id) } - @domain_blocking = { @account.id => current_account.domain_blocking?(@account.domain) } - - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def mute MuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unfollow UnfollowService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unblock UnblockService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unmute UnmuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end private @@ -59,12 +48,7 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end - def set_relationship - @following = Account.following_map([@account.id], current_user.account_id) - @followed_by = Account.followed_by_map([@account.id], current_user.account_id) - @blocking = Account.blocking_map([@account.id], current_user.account_id) - @muting = Account.muting_map([@account.id], current_user.account_id) - @requested = Account.requested_map([@account.id], current_user.account_id) - @domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id) + def relationships + AccountRelationshipsPresenter.new([@account.id], current_user.account_id) end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 98e908948..44a27b20a 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -5,6 +5,7 @@ class Api::V1::AppsController < Api::BaseController def create @app = Doorkeeper::Application.create!(application_options) + render json: @app, serializer: REST::ApplicationSerializer end private diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 1702953cf..a412e4341 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -9,6 +9,7 @@ class Api::V1::BlocksController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index fe0819a3f..92c0a62a9 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -9,14 +9,13 @@ class Api::V1::FavouritesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_favourites.tap do |statuses| - set_maps(statuses) - end + cached_favourites end def cached_favourites diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index eed22ef4f..b9f50d784 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -7,6 +7,7 @@ class Api::V1::FollowRequestsController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end def authorize diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index bcdb4e177..e01ae5c01 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - render :show + render json: @account, serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index ce2181879..1c6971c18 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -3,5 +3,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json - def show; end + def show + render json: {}, serializer: REST::InstanceSerializer + end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 25a331319..8a1992fca 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -11,6 +11,7 @@ class Api::V1::MediaController < Api::BaseController def create @media = current_account.media_attachments.create!(file: media_params[:file]) + render json: @media, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 2a353df03..0c43cb943 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -9,6 +9,7 @@ class Api::V1::MutesController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index a28e99f2f..8910b77e9 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -11,11 +11,12 @@ class Api::V1::NotificationsController < Api::BaseController def index @notifications = load_notifications - set_maps_for_notification_target_statuses + render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) end def show @notification = current_account.notifications.find(params[:id]) + render json: @notification, serializer: REST::NotificationSerializer end def clear @@ -46,10 +47,6 @@ class Api::V1::NotificationsController < Api::BaseController current_account.notifications.browserable(exclude_types) end - def set_maps_for_notification_target_statuses - set_maps target_statuses_from_notifications - end - def target_statuses_from_notifications @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status) end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8e7070d07..9592cd4bd 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -9,6 +9,7 @@ class Api::V1::ReportsController < Api::BaseController def index @reports = current_account.reports + render json: @reports, each_serializer: REST::ReportSerializer end def create @@ -20,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } - render :show + render json: @report, serializer: REST::ReportSerializer end private diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 8b832148c..1353682ea 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -6,7 +6,8 @@ class Api::V1::SearchController < Api::BaseController respond_to :json def index - @search = OpenStruct.new(search_results) + @search = Search.new(search_results) + render json: @search, serializer: REST::SearchSerializer end private diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index e58184939..f95cf9457 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index b6fb13cc0..4c4b0c160 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController def create @status = favourited_status - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController UnfavouriteWorker.perform_async(current_user.account_id, @status.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index eab88f2ef..a4bf0acdd 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController current_account.mute_conversation!(@conversation) @mutes_map = { @conversation.id => true } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy current_account.unmute_conversation!(@conversation) @mutes_map = { @conversation.id => false } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 43593d3c5..175217e6e 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index ee9c5b3a6..f7f4b5a5c 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController def create @status = ReblogService.new.call(current_user.account, status_for_reblog) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController authorize status_for_destroy, :unreblog? RemovalWorker.perform_async(status_for_destroy.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 9aa1cbc4d..9c7124d0f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -13,6 +13,7 @@ class Api::V1::StatusesController < Api::BaseController def show cached = Rails.cache.read(@status.cache_key) @status = cached unless cached.nil? + render json: @status, serializer: REST::StatusSerializer end def context @@ -21,15 +22,20 @@ class Api::V1::StatusesController < Api::BaseController loaded_ancestors = cache_collection(ancestors_results, Status) loaded_descendants = cache_collection(descendants_results, Status) - @context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants) - statuses = [@status] + @context[:ancestors] + @context[:descendants] + @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) + statuses = [@status] + @context.ancestors + @context.descendants - set_maps(statuses) + render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) end def card @card = PreviewCard.find_by(status: @status) - render_empty if @card.nil? + + if @card.nil? + render_empty + else + render json: @card, serializer: REST::PreviewCardSerializer + end end def create @@ -43,7 +49,7 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, idempotency: request.headers['Idempotency-Key']) - render :show + render json: @status, serializer: REST::StatusSerializer end def destroy diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index 511d2f65d..3dd27710c 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -9,15 +9,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_home_statuses.tap do |statuses| - set_maps(statuses) - end + cached_home_statuses end def cached_home_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 305451cc7..49887778e 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -7,15 +7,13 @@ class Api::V1::Timelines::PublicController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_public_statuses.tap do |statuses| - set_maps(statuses) - end + cached_public_statuses end def cached_public_statuses diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 50afca7c7..08db04a39 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +18,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end def load_statuses - cached_tagged_statuses.tap do |statuses| - set_maps(statuses) - end + cached_tagged_statuses end def cached_tagged_statuses diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb deleted file mode 100644 index 26adcb03a..000000000 --- a/app/lib/inline_rabl_scope.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class InlineRablScope - include RoutingHelper - - def initialize(account) - @account = account - end - - def current_user - @account.try(:user) - end - - def current_account - @account - end -end diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 8e04ad1d5..7cd9758ec 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -1,13 +1,33 @@ # frozen_string_literal: true class InlineRenderer - def self.render(status, current_account, template) - Rabl::Renderer.new( - template, - status, - view_path: 'app/views', - format: :json, - scope: InlineRablScope.new(current_account) - ).render + def initialize(object, current_account, template) + @object = object + @current_account = current_account + @template = template + end + + def render + case @template + when :status + serializer = REST::StatusSerializer + when :notification + serializer = REST::NotificationSerializer + else + return + end + + serializable_resource = ActiveModelSerializers::SerializableResource.new(@object, serializer: serializer, scope: current_user, scope_name: :current_user) + serializable_resource.as_json + end + + def self.render(object, current_account, template) + new(object, current_account, template).render + end + + private + + def current_user + @current_account&.user end end diff --git a/app/models/context.rb b/app/models/context.rb new file mode 100644 index 000000000..cc667999e --- /dev/null +++ b/app/models/context.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Context < ActiveModelSerializers::Model + attributes :ancestors, :descendants +end diff --git a/app/models/search.rb b/app/models/search.rb new file mode 100644 index 000000000..676c2a7f8 --- /dev/null +++ b/app/models/search.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Search < ActiveModelSerializers::Model + attributes :accounts, :statuses, :hashtags +end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb new file mode 100644 index 000000000..657807863 --- /dev/null +++ b/app/presenters/account_relationships_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AccountRelationshipsPresenter + attr_reader :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def initialize(account_ids, current_account_id) + @following = Account.following_map(account_ids, current_account_id) + @followed_by = Account.followed_by_map(account_ids, current_account_id) + @blocking = Account.blocking_map(account_ids, current_account_id) + @muting = Account.muting_map(account_ids, current_account_id) + @requested = Account.requested_map(account_ids, current_account_id) + @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) + end +end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb new file mode 100644 index 000000000..caf00791a --- /dev/null +++ b/app/presenters/status_relationships_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class StatusRelationshipsPresenter + attr_reader :reblogs_map, :favourites_map, :mutes_map + + def initialize(statuses, current_account_id = nil) + if current_account_id.nil? + @reblogs_map = {} + @favourites_map = {} + @mutes_map = {} + 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) + end + end +end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb new file mode 100644 index 000000000..012a4fd18 --- /dev/null +++ b/app/serializers/rest/account_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class REST::AccountSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :username, :acct, :display_name, :locked, :created_at, + :note, :url, :avatar, :avatar_static, :header, :header_static, + :followers_count, :following_count, :statuses_count + + def note + Formatter.instance.simplified_format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def avatar + full_asset_url(object.avatar_original_url) + end + + def avatar_static + full_asset_url(object.avatar_static_url) + end + + def header + full_asset_url(object.header_original_url) + end + + def header_static + full_asset_url(object.header_static_url) + end +end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb new file mode 100644 index 000000000..868a62f1e --- /dev/null +++ b/app/serializers/rest/application_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::ApplicationSerializer < ActiveModel::Serializer + attributes :id, :name, :website, :redirect_uri, + :client_id, :client_secret + + def client_id + object.uid + end + + def client_secret + object.secret + end +end diff --git a/app/serializers/rest/context_serializer.rb b/app/serializers/rest/context_serializer.rb new file mode 100644 index 000000000..44515c85d --- /dev/null +++ b/app/serializers/rest/context_serializer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class REST::ContextSerializer < ActiveModel::Serializer + has_many :ancestors, serializer: REST::StatusSerializer + has_many :descendants, serializer: REST::StatusSerializer +end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb new file mode 100644 index 000000000..8e32f9cb3 --- /dev/null +++ b/app/serializers/rest/instance_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::InstanceSerializer < ActiveModel::Serializer + attributes :uri, :title, :description, :email, + :version, :urls + + def uri + Rails.configuration.x.local_domain + end + + def title + Setting.site_title + end + + def description + Setting.site_description + end + + def email + Setting.site_contact_email + end + + def version + Mastodon::Version.to_s + end + + def urls + { streaming_api: Rails.configuration.x.streaming_api_base_url } + end +end diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb new file mode 100644 index 000000000..9b07a686e --- /dev/null +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class REST::MediaAttachmentSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :type, :url, :preview_url, + :remote_url, :text_url, :meta + + def url + full_asset_url(object.file.url(:original)) + end + + def preview_url + full_asset_url(object.file.url(:small)) + end + + def text_url + medium_url(object.id) + end + + def meta + object.file.meta + end +end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb new file mode 100644 index 000000000..97fadf32e --- /dev/null +++ b/app/serializers/rest/notification_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::NotificationSerializer < ActiveModel::Serializer + attributes :id, :type, :created_at + + belongs_to :from_account, key: :account, serializer: REST::AccountSerializer + belongs_to :status, if: :status_type?, serializer: REST::StatusSerializer + + def status_type? + [:favourite, :reblog, :mention].include?(object.type) + end +end diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb new file mode 100644 index 000000000..9c460332c --- /dev/null +++ b/app/serializers/rest/preview_card_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::PreviewCardSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :url, :title, :description, :type, + :author_name, :author_url, :provider_name, + :provider_url, :html, :width, :height, + :image + + def image + object.image? ? full_asset_url(object.image.url(:original)) : nil + end +end diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb new file mode 100644 index 000000000..1d431aa1b --- /dev/null +++ b/app/serializers/rest/relationship_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::RelationshipSerializer < ActiveModel::Serializer + attributes :id, :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def following + instance_options[:relationships].following[object.id] || false + end + + def followed_by + instance_options[:relationships].followed_by[object.id] || false + end + + def blocking + instance_options[:relationships].blocking[object.id] || false + end + + def muting + instance_options[:relationships].muting[object.id] || false + end + + def requested + instance_options[:relationships].requested[object.id] || false + end + + def domain_blocking + instance_options[:relationships].domain_blocking[object.id] || false + end +end diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb new file mode 100644 index 000000000..0c6bd6556 --- /dev/null +++ b/app/serializers/rest/report_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken +end diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb new file mode 100644 index 000000000..157f543ae --- /dev/null +++ b/app/serializers/rest/search_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::SearchSerializer < ActiveModel::Serializer + attributes :hashtags + + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer + + def hashtags + object.hashtags.map(&:name) + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb new file mode 100644 index 000000000..246b12a90 --- /dev/null +++ b/app/serializers/rest/status_serializer.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +class REST::StatusSerializer < ActiveModel::Serializer + attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, + :sensitive, :spoiler_text, :visibility, :language, + :uri, :content, :url, :reblogs_count, :favourites_count + + attribute :favourited, if: :current_user? + attribute :reblogged, if: :current_user? + attribute :muted, if: :current_user? + + belongs_to :reblog, serializer: REST::StatusSerializer + belongs_to :application + belongs_to :account, serializer: REST::AccountSerializer + + has_many :media_attachments, serializer: REST::MediaAttachmentSerializer + has_many :mentions + has_many :tags + + def current_user? + !current_user.nil? + end + + def uri + TagManager.instance.uri_for(object) + end + + def content + Formatter.instance.format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def favourited + if instance_options && instance_options[:relationships] + instance_options[:relationships].favourites_map[object.id] || false + else + current_user.account.favourited?(object) + end + end + + def reblogged + if instance_options && instance_options[:relationships] + instance_options[:relationships].reblogs_map[object.id] || false + else + current_user.account.reblogged?(object) + end + end + + def muted + if instance_options && instance_options[:relationships] + instance_options[:relationships].mutes_map[object.conversation_id] || false + else + current_user.account.muting_conversation?(object.conversation) + end + end + + class ApplicationSerializer < ActiveModel::Serializer + attributes :name, :website + end + + class MentionSerializer < ActiveModel::Serializer + attributes :id, :username, :url, :acct + + def id + object.account_id + end + + def username + object.account_username + end + + def url + TagManager.instance.url_for(object.account) + end + + def acct + object.account_acct + end + end + + class TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url + + def url + tag_url(object) + end + end +end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3b74696d5..47a47a735 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -54,7 +54,7 @@ class FanOutOnWriteService < BaseService end def render_anonymous_payload(status) - @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show') + @payload = InlineRenderer.render(status, nil, :status) @payload = Oj.dump(event: :update, payload: @payload) end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 422d5f97e..407d385ea 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -60,7 +60,7 @@ class NotifyService < BaseService def create_notification @notification.save! return unless @notification.browserable? - Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show'))) + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) end def send_email diff --git a/app/views/api/v1/accounts/index.rabl b/app/views/api/v1/accounts/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/accounts/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl deleted file mode 100644 index 4f7763d9d..000000000 --- a/app/views/api/v1/accounts/relationship.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @account - -attribute :id -node(:following) { |account| @following[account.id] || false } -node(:followed_by) { |account| @followed_by[account.id] || false } -node(:blocking) { |account| @blocking[account.id] || false } -node(:muting) { |account| @muting[account.id] || false } -node(:requested) { |account| @requested[account.id] || false } -node(:domain_blocking) { |account| @domain_blocking[account.id] || false } diff --git a/app/views/api/v1/accounts/relationships/index.rabl b/app/views/api/v1/accounts/relationships/index.rabl deleted file mode 100644 index 022ea2ac4..000000000 --- a/app/views/api/v1/accounts/relationships/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/relationship' diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl deleted file mode 100644 index 8826aa22d..000000000 --- a/app/views/api/v1/accounts/show.rabl +++ /dev/null @@ -1,12 +0,0 @@ -object @account - -attributes :id, :username, :acct, :display_name, :locked, :created_at - -node(:note) { |account| Formatter.instance.simplified_format(account) } -node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar_original_url) } -node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } -node(:header) { |account| full_asset_url(account.header_original_url) } -node(:header_static) { |account| full_asset_url(account.header_static_url) } - -attributes :followers_count, :following_count, :statuses_count diff --git a/app/views/api/v1/accounts/statuses/index.rabl b/app/views/api/v1/accounts/statuses/index.rabl deleted file mode 100644 index 44d29d91b..000000000 --- a/app/views/api/v1/accounts/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/apps/create.rabl b/app/views/api/v1/apps/create.rabl deleted file mode 100644 index 1ff6469a4..000000000 --- a/app/views/api/v1/apps/create.rabl +++ /dev/null @@ -1,4 +0,0 @@ -object @app -attributes :id, :redirect_uri -node(:client_id) { |app| app.uid } -node(:client_secret) { |app| app.secret } diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl deleted file mode 100644 index 6d9e607db..000000000 --- a/app/views/api/v1/apps/show.rabl +++ /dev/null @@ -1,3 +0,0 @@ -object @application - -attributes :name, :website diff --git a/app/views/api/v1/blocks/index.rabl b/app/views/api/v1/blocks/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/blocks/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/favourites/index.rabl b/app/views/api/v1/favourites/index.rabl deleted file mode 100644 index 44d29d91b..000000000 --- a/app/views/api/v1/favourites/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/follow_requests/index.rabl b/app/views/api/v1/follow_requests/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/follow_requests/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/follows/show.rabl b/app/views/api/v1/follows/show.rabl deleted file mode 100644 index e07106164..000000000 --- a/app/views/api/v1/follows/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @account -extends('api/v1/accounts/show') diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl deleted file mode 100644 index 05fb65031..000000000 --- a/app/views/api/v1/instances/show.rabl +++ /dev/null @@ -1,10 +0,0 @@ -object false - -node(:uri) { site_hostname } -node(:title) { Setting.site_title } -node(:description) { Setting.site_description } -node(:email) { Setting.site_contact_email } -node(:version) { Mastodon::Version.to_s } -node :urls do - { :streaming_api => Rails.configuration.x.streaming_api_base_url } -end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl deleted file mode 100644 index 53c13bbda..000000000 --- a/app/views/api/v1/media/create.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @media -attribute :id, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| medium_url(media) } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/mutes/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/notifications/index.rabl b/app/views/api/v1/notifications/index.rabl deleted file mode 100644 index 6abc3da36..000000000 --- a/app/views/api/v1/notifications/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @notifications -extends 'api/v1/notifications/show' diff --git a/app/views/api/v1/notifications/show.rabl b/app/views/api/v1/notifications/show.rabl deleted file mode 100644 index ca34f2d5d..000000000 --- a/app/views/api/v1/notifications/show.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @notification - -attributes :id, :type, :created_at - -child from_account: :account do - extends 'api/v1/accounts/show' -end - -node(:status, if: lambda { |n| [:favourite, :reblog, :mention].include?(n.type) }) do |n| - partial 'api/v1/statuses/show', object: n.target_status -end diff --git a/app/views/api/v1/reports/index.rabl b/app/views/api/v1/reports/index.rabl deleted file mode 100644 index 4f0794027..000000000 --- a/app/views/api/v1/reports/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @reports -extends 'api/v1/reports/show' diff --git a/app/views/api/v1/reports/show.rabl b/app/views/api/v1/reports/show.rabl deleted file mode 100644 index 006db51e3..000000000 --- a/app/views/api/v1/reports/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @report -attributes :id, :action_taken diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl deleted file mode 100644 index 8d1640f2d..000000000 --- a/app/views/api/v1/search/index.rabl +++ /dev/null @@ -1,13 +0,0 @@ -object @search - -child :accounts, object_root: false do - extends 'api/v1/accounts/show' -end - -node(:hashtags) do |search| - search.hashtags.map(&:name) -end - -child :statuses, object_root: false do - extends 'api/v1/statuses/show' -end diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl deleted file mode 100644 index 07ac31888..000000000 --- a/app/views/api/v1/statuses/_media.rabl +++ /dev/null @@ -1,6 +0,0 @@ -attributes :id, :remote_url, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| media.local? ? medium_url(media) : nil } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/statuses/_mention.rabl b/app/views/api/v1/statuses/_mention.rabl deleted file mode 100644 index 8c95fc9bd..000000000 --- a/app/views/api/v1/statuses/_mention.rabl +++ /dev/null @@ -1,4 +0,0 @@ -node(:url) { |mention| TagManager.instance.url_for(mention.account) } -node(:acct) { |mention| mention.account_acct } -node(:id) { |mention| mention.account_id } -node(:username) { |mention| mention.account_username } diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl deleted file mode 100644 index fe3ec89ab..000000000 --- a/app/views/api/v1/statuses/_show.rabl +++ /dev/null @@ -1,29 +0,0 @@ -attributes :id, :created_at, :in_reply_to_id, - :in_reply_to_account_id, :sensitive, - :spoiler_text, :visibility, :language - -node(:uri) { |status| TagManager.instance.uri_for(status) } -node(:content) { |status| Formatter.instance.format(status) } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count } -node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count } - -child :application do - extends 'api/v1/apps/show' -end - -child :account do - extends 'api/v1/accounts/show' -end - -child :media_attachments, object_root: false do - extends 'api/v1/statuses/_media' -end - -child :mentions, object_root: false do - extends 'api/v1/statuses/_mention' -end - -child :tags, object_root: false do - extends 'api/v1/statuses/_tags' -end diff --git a/app/views/api/v1/statuses/_tags.rabl b/app/views/api/v1/statuses/_tags.rabl deleted file mode 100644 index 25e7b0fac..000000000 --- a/app/views/api/v1/statuses/_tags.rabl +++ /dev/null @@ -1,2 +0,0 @@ -attribute :name -node(:url) { |tag| tag_url(tag) } diff --git a/app/views/api/v1/statuses/accounts.rabl b/app/views/api/v1/statuses/accounts.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/statuses/accounts.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/statuses/card.rabl b/app/views/api/v1/statuses/card.rabl deleted file mode 100644 index 5d8d7af3b..000000000 --- a/app/views/api/v1/statuses/card.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @card - -attributes :url, :title, :description, :type, - :author_name, :author_url, :provider_name, - :provider_url, :html, :width, :height - -node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } diff --git a/app/views/api/v1/statuses/context.rabl b/app/views/api/v1/statuses/context.rabl deleted file mode 100644 index 0b62f26d5..000000000 --- a/app/views/api/v1/statuses/context.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @context - -node :ancestors do |context| - partial 'api/v1/statuses/index', object: context.ancestors -end - -node :descendants do |context| - partial 'api/v1/statuses/index', object: context.descendants -end diff --git a/app/views/api/v1/statuses/index.rabl b/app/views/api/v1/statuses/index.rabl deleted file mode 100644 index 0a0ed13c5..000000000 --- a/app/views/api/v1/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/api/v1/statuses/show.rabl b/app/views/api/v1/statuses/show.rabl deleted file mode 100644 index 4b33fb2c3..000000000 --- a/app/views/api/v1/statuses/show.rabl +++ /dev/null @@ -1,15 +0,0 @@ -object @status - -extends 'api/v1/statuses/_show' - -node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } -node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } -node(:muted, if: proc { !current_account.nil? }) { |status| defined?(@mutes_map) ? @mutes_map[status.conversation_id] : current_account.muting_conversation?(status.conversation) } - -child reblog: :reblog do - extends 'api/v1/statuses/_show' - - node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } - node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } - node(:muted, if: proc { !current_account.nil? }) { false } -end diff --git a/app/views/api/v1/timelines/show.rabl b/app/views/api/v1/timelines/show.rabl deleted file mode 100644 index 0a0ed13c5..000000000 --- a/app/views/api/v1/timelines/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index 291ff806b..c428a5a1f 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -24,8 +24,8 @@ end node(:accounts) do store = {} - store[current_account.id] = partial('api/v1/accounts/show', object: current_account) - store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil? + store[current_account.id] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer) + store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) unless @admin.nil? store end diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index fbcdcf634..697cbd6a6 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -6,7 +6,7 @@ class PushUpdateWorker def perform(account_id, status_id) account = Account.find(account_id) status = Status.find(status_id) - message = InlineRenderer.render(status, account, 'api/v1/statuses/show') + message = InlineRenderer.render(status, account, :status) Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) rescue ActiveRecord::RecordNotFound diff --git a/spec/lib/inline_rabl_scope_spec.rb b/spec/lib/inline_rabl_scope_spec.rb deleted file mode 100644 index 3fff176e4..000000000 --- a/spec/lib/inline_rabl_scope_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe InlineRablScope do - describe '#current_account' do - it 'returns the given account' do - account = Fabricate(:account) - expect(InlineRablScope.new(account).current_account).to eq account - end - end - - describe '#current_user' do - it 'returns nil if the given account is nil' do - expect(InlineRablScope.new(nil).current_user).to eq nil - end - - it 'returns user of account if the given account is not nil' do - user = Fabricate(:user) - expect(InlineRablScope.new(user.account).current_user).to eq user - end - end -end -- cgit From 348d6f5e7551e632e7dea41e61c40f79aac59be9 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sat, 8 Jul 2017 00:06:02 +0200 Subject: Lazy load components (#3879) * feat: Lazy-load routes * feat: Lazy-load modals * feat: Lazy-load columns * refactor: Simplify Bundle API * feat: Optimize bundles * feat: Prevent flashing the waiting state * feat: Preload commonly used bundles * feat: Lazy load Compose reducers * feat: Lazy load Notifications reducer * refactor: Move all dynamic imports into one file * fix: Minor bugs * fix: Manually hydrate the lazy-loaded reducers * refactor: Move all dynamic imports to async-components * fix: Loading modal style * refactor: Avoid converting the raw state for each lazy hydration * refactor: Remove unused component * refactor: Maintain modal name * fix: Add as=script to preload link * chore: Fix lint error * fix(components/bundle): Check if timestamp is set when computing elapsed * fix: Load compose reducers for the onboarding modal --- app/javascript/mastodon/actions/bundles.js | 25 ++++ app/javascript/mastodon/actions/store.js | 8 ++ app/javascript/mastodon/components/status.js | 27 +++- app/javascript/mastodon/containers/mastodon.js | 5 +- .../compose/components/emoji_picker_dropdown.js | 3 +- .../mastodon/features/ui/components/bundle.js | 96 ++++++++++++++ .../features/ui/components/bundle_column_error.js | 44 +++++++ .../features/ui/components/bundle_modal_error.js | 53 ++++++++ .../features/ui/components/column_loading.js | 13 ++ .../features/ui/components/columns_area.js | 28 ++-- .../features/ui/components/modal_loading.js | 20 +++ .../mastodon/features/ui/components/modal_root.js | 51 +++++--- .../features/ui/containers/bundle_container.js | 19 +++ app/javascript/mastodon/features/ui/index.js | 89 +++++-------- .../mastodon/features/ui/util/async-components.js | 143 +++++++++++++++++++++ .../features/ui/util/react_router_helpers.js | 65 ++++++++++ app/javascript/mastodon/reducers/compose.js | 4 +- app/javascript/mastodon/reducers/index.js | 21 +-- .../mastodon/reducers/media_attachments.js | 4 +- app/javascript/mastodon/store/configureStore.js | 25 +++- app/javascript/styles/components.scss | 31 ++++- app/views/layouts/application.html.haml | 17 +++ 22 files changed, 680 insertions(+), 111 deletions(-) create mode 100644 app/javascript/mastodon/actions/bundles.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_modal_error.js create mode 100644 app/javascript/mastodon/features/ui/components/column_loading.js create mode 100644 app/javascript/mastodon/features/ui/components/modal_loading.js create mode 100644 app/javascript/mastodon/features/ui/containers/bundle_container.js create mode 100644 app/javascript/mastodon/features/ui/util/async-components.js create mode 100644 app/javascript/mastodon/features/ui/util/react_router_helpers.js (limited to 'app/views') diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js new file mode 100644 index 000000000..ecc9c8f7d --- /dev/null +++ b/app/javascript/mastodon/actions/bundles.js @@ -0,0 +1,25 @@ +export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; +export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; +export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; + +export function fetchBundleRequest(skipLoading) { + return { + type: BUNDLE_FETCH_REQUEST, + skipLoading, + }; +} + +export function fetchBundleSuccess(skipLoading) { + return { + type: BUNDLE_FETCH_SUCCESS, + skipLoading, + }; +} + +export function fetchBundleFail(error, skipLoading) { + return { + type: BUNDLE_FETCH_FAIL, + error, + skipLoading, + }; +} diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 601cea001..08c2810ca 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,6 +1,7 @@ import Immutable from 'immutable'; export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => Immutable.fromJS(rawState, (k, v) => @@ -15,3 +16,10 @@ export function hydrateStore(rawState) { state, }; }; + +export function hydrateStoreLazy(name, state) { + return { + type: `${STORE_HYDRATE_LAZY}-${name}`, + state, + }; +}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index ff574ab3d..18ce0198e 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -5,8 +5,6 @@ import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; -import MediaGallery from './media_gallery'; -import VideoPlayer from './video_player'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; @@ -14,6 +12,11 @@ import emojify from '../emoji'; import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; + +// We use the component (and not the container) since we do not want +// to use the progress bar to show download progress +import Bundle from '../features/ui/components/bundle'; export default class Status extends ImmutablePureComponent { @@ -154,6 +157,14 @@ export default class Status extends ImmutablePureComponent { this.setState({ isExpanded: !this.state.isExpanded }); }; + renderLoadingMediaGallery () { + return
; + } + + renderLoadingVideoPlayer () { + return
; + } + render () { let media = null; let statusAvatar; @@ -201,9 +212,17 @@ export default class Status extends ImmutablePureComponent { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + media = ( + + {Component => } + + ); } else { - media = ; + media = ( + + {Component => } + + ); } } diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 3bd89902f..6e79f9e4f 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -22,9 +22,10 @@ import { getLocale } from '../locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); +export const store = configureStore(); const initialState = JSON.parse(document.getElementById('initial-state').textContent); -store.dispatch(hydrateStore(initialState)); +export const hydrateAction = hydrateStore(initialState); +store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c83dbb63e..83c66a5d5 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.setState({ active: true }); if (!EmojiPicker) { this.setState({ loading: true }); - import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => { + EmojiPickerAsync().then(TheEmojiPicker => { EmojiPicker = TheEmojiPicker.default; this.setState({ loading: false }); }).catch(() => { diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js new file mode 100644 index 000000000..e69a32f47 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const emptyComponent = () => null; +const noop = () => { }; + +class Bundle extends React.Component { + + static propTypes = { + fetchComponent: PropTypes.func.isRequired, + loading: PropTypes.func, + error: PropTypes.func, + children: PropTypes.func.isRequired, + renderDelay: PropTypes.number, + onRender: PropTypes.func, + onFetch: PropTypes.func, + onFetchSuccess: PropTypes.func, + onFetchFail: PropTypes.func, + } + + static defaultProps = { + loading: emptyComponent, + error: emptyComponent, + renderDelay: 0, + onRender: noop, + onFetch: noop, + onFetchSuccess: noop, + onFetchFail: noop, + } + + state = { + mod: undefined, + forceRender: false, + } + + componentWillMount() { + this.load(this.props); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.fetchComponent !== this.props.fetchComponent) { + this.load(nextProps); + } + } + + componentDidUpdate () { + this.props.onRender(); + } + + componentWillUnmount () { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + + load = (props) => { + const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; + + this.setState({ mod: undefined }); + onFetch(); + + if (renderDelay !== 0) { + this.timestamp = new Date(); + this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); + } + + return fetchComponent() + .then((mod) => { + this.setState({ mod: mod.default }); + onFetchSuccess(); + }) + .catch((error) => { + this.setState({ mod: null }); + onFetchFail(error); + }); + } + + render() { + const { loading: Loading, error: Error, children, renderDelay } = this.props; + const { mod, forceRender } = this.state; + const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; + + if (mod === undefined) { + return (elapsed >= renderDelay || forceRender) ? : null; + } + + if (mod === null) { + return ; + } + + return children(mod); + } + +} + +export default Bundle; diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js new file mode 100644 index 000000000..cd124746a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import Column from './column'; +import ColumnHeader from './column_header'; +import ColumnBackButtonSlim from '../../../components/column_back_button_slim'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, + body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, +}); + +class BundleColumnError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { intl: { formatMessage } } = this.props; + + return ( + + + +
+ + {formatMessage(messages.body)} +
+
+ ); + } + +} + +export default injectIntl(BundleColumnError); diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js new file mode 100644 index 000000000..928bfe1f7 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, + close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, +}); + +class BundleModalError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { onClose, intl: { formatMessage } } = this.props; + + // Keep the markup in sync with + // (make sure they have the same dimensions) + return ( +
+
+ + {formatMessage(messages.error)} +
+ +
+
+ +
+
+
+ ); + } + +} + +export default injectIntl(BundleModalError); diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js new file mode 100644 index 000000000..9bb9c14a1 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; + +const ColumnLoading = () => ( + + +
+ +); + +export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 01167b6e5..5fa27599f 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -2,15 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + import ReactSwipeable from 'react-swipeable'; -import HomeTimeline from '../../home_timeline'; -import Notifications from '../../notifications'; -import PublicTimeline from '../../public_timeline'; -import CommunityTimeline from '../../community_timeline'; -import HashtagTimeline from '../../hashtag_timeline'; -import Compose from '../../compose'; import { getPreviousLink, getNextLink } from './tabs_bar'; +import BundleContainer from '../containers/bundle_container'; +import ColumnLoading from './column_loading'; +import BundleColumnError from './bundle_column_error'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components'; + const componentMap = { 'COMPOSE': Compose, 'HOME': HomeTimeline, @@ -48,6 +48,14 @@ export default class ColumnsArea extends ImmutablePureComponent { } }; + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + render () { const { columns, children, singleColumn } = this.props; @@ -62,9 +70,13 @@ export default class ColumnsArea extends ImmutablePureComponent { return (
{columns.map(column => { - const SpecificComponent = componentMap[column.get('id')]; const params = column.get('params', null) === null ? null : column.get('params').toJS(); - return ; + + return ( + + {SpecificComponent => } + + ); })} {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js new file mode 100644 index 000000000..f403ca4c9 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/modal_loading.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import LoadingIndicator from '../../../components/loading_indicator'; + +// Keep the markup in sync with +// (make sure they have the same dimensions) +const ModalLoading = () => ( +
+
+ +
+
+
+
+
+
+); + +export default ModalLoading; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 48b048eb7..085299038 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,13 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import MediaModal from './media_modal'; -import OnboardingModal from './onboarding_modal'; -import VideoModal from './video_modal'; -import BoostModal from './boost_modal'; -import ConfirmationModal from './confirmation_modal'; -import ReportModal from './report_modal'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import BundleContainer from '../containers/bundle_container'; +import BundleModalError from './bundle_modal_error'; +import ModalLoading from './modal_loading'; +import { + MediaModal, + OnboardingModal, + VideoModal, + BoostModal, + ConfirmationModal, + ReportModal, +} from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, @@ -49,6 +54,22 @@ export default class ModalRoot extends React.PureComponent { return { opacity: spring(0), scale: spring(0.98) }; } + renderModal = (SpecificComponent) => { + const { props, onClose } = this.props; + + return ; + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + const { onClose } = this.props; + + return ; + } + render () { const { type, props, onClose } = this.props; const visible = !!type; @@ -70,18 +91,14 @@ export default class ModalRoot extends React.PureComponent { > {interpolatedStyles =>
- {interpolatedStyles.map(({ key, data: { type, props }, style }) => { - const SpecificComponent = MODAL_COMPONENTS[type]; - - return ( -
-
-
- -
+ {interpolatedStyles.map(({ key, data: { type }, style }) => ( +
+
+
+ {this.renderModal}
- ); - })} +
+ ))}
} diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js new file mode 100644 index 000000000..7e3f0c3a6 --- /dev/null +++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; + +import Bundle from '../components/bundle'; + +import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; + +const mapDispatchToProps = dispatch => ({ + onFetch () { + dispatch(fetchBundleRequest()); + }, + onFetchSuccess () { + dispatch(fetchBundleSuccess()); + }, + onFetchFail (error) { + dispatch(fetchBundleFail(error)); + }, +}); + +export default connect(null, mapDispatchToProps)(Bundle); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 54e623d99..6057d8797 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,7 +1,5 @@ import React from 'react'; import classNames from 'classnames'; -import Switch from 'react-router-dom/Switch'; -import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; import NotificationsContainer from './containers/notifications_container'; import PropTypes from 'prop-types'; @@ -14,64 +12,40 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; +import { store } from '../../containers/mastodon'; import ColumnsAreaContainer from './containers/columns_area_container'; -import Status from '../../features/status'; -import GettingStarted from '../../features/getting_started'; -import PublicTimeline from '../../features/public_timeline'; -import CommunityTimeline from '../../features/community_timeline'; -import AccountTimeline from '../../features/account_timeline'; -import AccountGallery from '../../features/account_gallery'; -import HomeTimeline from '../../features/home_timeline'; -import Compose from '../../features/compose'; -import Followers from '../../features/followers'; -import Following from '../../features/following'; -import Reblogs from '../../features/reblogs'; -import Favourites from '../../features/favourites'; -import HashtagTimeline from '../../features/hashtag_timeline'; -import Notifications from '../../features/notifications'; -import FollowRequests from '../../features/follow_requests'; -import GenericNotFound from '../../features/generic_not_found'; -import FavouritedStatuses from '../../features/favourited_statuses'; -import Blocks from '../../features/blocks'; -import Mutes from '../../features/mutes'; - -// Small wrapper to pass multiColumn to the route components -const WrappedSwitch = ({ multiColumn, children }) => ( - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - -); - -WrappedSwitch.propTypes = { - multiColumn: PropTypes.bool, - children: PropTypes.node, -}; - -// Small Wraper to extract the params from the route and pass -// them to the rendered component, together with the content to -// be rendered inside (the children) -class WrappedRoute extends React.Component { - - static propTypes = { - component: PropTypes.func.isRequired, - content: PropTypes.node, - multiColumn: PropTypes.bool, - } - - renderComponent = ({ match: { params } }) => { - const { component: Component, content, multiColumn } = this.props; - - return {content}; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return ; - } +import { + Compose, + Status, + GettingStarted, + PublicTimeline, + CommunityTimeline, + AccountTimeline, + AccountGallery, + HomeTimeline, + Followers, + Following, + Reblogs, + Favourites, + HashtagTimeline, + Notifications as AsyncNotifications, + FollowRequests, + GenericNotFound, + FavouritedStatuses, + Blocks, + Mutes, +} from './util/async-components'; + +const Notifications = () => AsyncNotifications().then(component => { + store.dispatch(refreshNotifications()); + return component; +}); -} +// Dummy import, to make sure that ends up in the application bundle. +// Without this it ends up in ~8 very commonly used bundles. +import '../../components/status'; const mapStateToProps = state => ({ systemFontUi: state.getIn(['meta', 'system_font_ui']), @@ -162,7 +136,6 @@ export default class UI extends React.PureComponent { document.addEventListener('dragend', this.handleDragEnd, false); this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); } componentWillUnmount () { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js new file mode 100644 index 000000000..c9f81136d --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -0,0 +1,143 @@ +import { store } from '../../../containers/mastodon'; +import { injectAsyncReducer } from '../../../store/configureStore'; + +// NOTE: When lazy-loading reducers, make sure to add them +// to application.html.haml (if the component is preloaded there) + +export function EmojiPicker () { + return import(/* webpackChunkName: "emojione_picker" */'emojione-picker'); +} + +export function Compose () { + return Promise.all([ + import(/* webpackChunkName: "features/compose" */'../../compose'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'), + ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + injectAsyncReducer(store, 'search', searchReducer.default); + + return component; + }); +} + +export function Notifications () { + return Promise.all([ + import(/* webpackChunkName: "features/notifications" */'../../notifications'), + import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'), + ]).then(([component, notificationsReducer]) => { + injectAsyncReducer(store, 'notifications', notificationsReducer.default); + + return component; + }); +} + +export function HomeTimeline () { + return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline'); +} + +export function PublicTimeline () { + return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); +} + +export function CommunityTimeline () { + return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); +} + +export function HashtagTimeline () { + return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); +} + +export function Status () { + return import(/* webpackChunkName: "features/status" */'../../status'); +} + +export function GettingStarted () { + return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); +} + +export function AccountTimeline () { + return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); +} + +export function AccountGallery () { + return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery'); +} + +export function Followers () { + return import(/* webpackChunkName: "features/followers" */'../../followers'); +} + +export function Following () { + return import(/* webpackChunkName: "features/following" */'../../following'); +} + +export function Reblogs () { + return import(/* webpackChunkName: "features/reblogs" */'../../reblogs'); +} + +export function Favourites () { + return import(/* webpackChunkName: "features/favourites" */'../../favourites'); +} + +export function FollowRequests () { + return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); +} + +export function GenericNotFound () { + return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found'); +} + +export function FavouritedStatuses () { + return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); +} + +export function Blocks () { + return import(/* webpackChunkName: "features/blocks" */'../../blocks'); +} + +export function Mutes () { + return import(/* webpackChunkName: "features/mutes" */'../../mutes'); +} + +export function MediaModal () { + return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal'); +} + +export function OnboardingModal () { + return Promise.all([ + import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + ]).then(([component, composeReducer, mediaAttachmentsReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + return component; + }); +} + +export function VideoModal () { + return import(/* webpackChunkName: "modals/video_modal" */'../components/video_modal'); +} + +export function BoostModal () { + return import(/* webpackChunkName: "modals/boost_modal" */'../components/boost_modal'); +} + +export function ConfirmationModal () { + return import(/* webpackChunkName: "modals/confirmation_modal" */'../components/confirmation_modal'); +} + +export function ReportModal () { + return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); +} + +export function MediaGallery () { + return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); +} + +export function VideoPlayer () { + return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); +} diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js new file mode 100644 index 000000000..e33a6df6f --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Switch from 'react-router-dom/Switch'; +import Route from 'react-router-dom/Route'; + +import ColumnLoading from '../components/column_loading'; +import BundleColumnError from '../components/bundle_column_error'; +import BundleContainer from '../containers/bundle_container'; + +// Small wrapper to pass multiColumn to the route components +export const WrappedSwitch = ({ multiColumn, children }) => ( + + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + +); + +WrappedSwitch.propTypes = { + multiColumn: PropTypes.bool, + children: PropTypes.node, +}; + +// Small Wraper to extract the params from the route and pass +// them to the rendered component, together with the content to +// be rendered inside (the children) +export class WrappedRoute extends React.Component { + + static propTypes = { + component: PropTypes.func.isRequired, + content: PropTypes.node, + multiColumn: PropTypes.bool, + } + + renderComponent = ({ match }) => { + this.match = match; // Needed for this.renderBundle + + const { component } = this.props; + + return ( + + {this.renderBundle} + + ); + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + + renderBundle = (Component) => { + const { match: { params }, props: { content, multiColumn } } = this; + + return {content}; + } + + render () { + const { component: Component, content, ...rest } = this.props; + + return ; + } + +} diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index d0b47a85c..09db95e2d 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -23,7 +23,7 @@ import { COMPOSE_EMOJI_INSERT, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; import uuid from '../uuid'; @@ -134,7 +134,7 @@ const privacyPreference = (a, b) => { export default function compose(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-compose`: return clearAll(state.merge(action.state.get('compose'))); case COMPOSE_MOUNT: return state.set('mounted', true); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index be402a16b..79062f2f9 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -1,7 +1,6 @@ import { combineReducers } from 'redux-immutable'; import timelines from './timelines'; import meta from './meta'; -import compose from './compose'; import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; import modal from './modal'; @@ -9,20 +8,16 @@ import user_lists from './user_lists'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; import statuses from './statuses'; -import media_attachments from './media_attachments'; import relationships from './relationships'; -import search from './search'; -import notifications from './notifications'; import settings from './settings'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; import contexts from './contexts'; -export default combineReducers({ +const reducers = { timelines, meta, - compose, alerts, loadingBar: loadingBarReducer, modal, @@ -30,13 +25,19 @@ export default combineReducers({ status_lists, accounts, accounts_counters, - media_attachments, statuses, relationships, - search, - notifications, settings, cards, reports, contexts, -}); +}; + +export function createReducer(asyncReducers) { + return combineReducers({ + ...reducers, + ...asyncReducers, + }); +} + +export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index 85bea4f0b..d17d465aa 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,4 +1,4 @@ -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -7,7 +7,7 @@ const initialState = Immutable.Map({ export default function meta(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-media_attachments`: return state.merge(action.state.get('media_attachments')); default: return state; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 1376d4cba..0fe29f031 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,15 +1,36 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import appReducer from '../reducers'; +import appReducer, { createReducer } from '../reducers'; +import { hydrateStoreLazy } from '../actions/store'; +import { hydrateAction } from '../containers/mastodon'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( + const store = createStore(appReducer, compose(applyMiddleware( thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware() ), window.devToolsExtension ? window.devToolsExtension() : f => f)); + + store.asyncReducers = { }; + + return store; }; + +export function injectAsyncReducer(store, name, asyncReducer) { + if (!store.asyncReducers[name]) { + // Keep track that we injected this reducer + store.asyncReducers[name] = asyncReducer; + + // Add the current reducer to the store + store.replaceReducer(createReducer(store.asyncReducers)); + + // The state this reducer handles defaults to its initial state (stored inside the reducer) + // But that state may be out of date because of the server-side hydration, so we replay + // the hydration action but only for this reducer (all async reducers must listen for this dynamic action) + store.dispatch(hydrateStoreLazy(name, hydrateAction.state)); + } +} diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index a87aa5d79..9b500c7ad 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2300,7 +2300,8 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } -.empty-column-indicator { +.empty-column-indicator, +.error-column { color: lighten($ui-base-color, 20%); background: $ui-base-color; text-align: center; @@ -2326,6 +2327,10 @@ button.icon-button.active i.fa-retweet { } } +.error-column { + flex-direction: column; +} + @keyframes pulse { 0% { opacity: 1; @@ -2909,7 +2914,8 @@ button.icon-button.active i.fa-retweet { z-index: 100; } -.onboarding-modal { +.onboarding-modal, +.error-modal { background: $ui-secondary-color; color: $ui-base-color; border-radius: 8px; @@ -2918,7 +2924,8 @@ button.icon-button.active i.fa-retweet { flex-direction: column; } -.onboarding-modal__pager { +.onboarding-modal__pager, +.error-modal__body { height: 80vh; width: 80vw; max-width: 520px; @@ -2943,6 +2950,14 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + @media screen and (max-width: 550px) { .onboarding-modal { width: 100%; @@ -2959,7 +2974,8 @@ button.icon-button.active i.fa-retweet { } } -.onboarding-modal__paginator { +.onboarding-modal__paginator, +.error-modal__footer { flex: 0 0 auto; background: darken($ui-secondary-color, 8%); display: flex; @@ -2969,7 +2985,8 @@ button.icon-button.active i.fa-retweet { min-width: 33px; } - .onboarding-modal__nav { + .onboarding-modal__nav, + .error-modal__nav { color: darken($ui-secondary-color, 34%); background-color: transparent; border: 0; @@ -2992,6 +3009,10 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__footer { + justify-content: center; +} + .onboarding-modal__dots { flex: 1 1 auto; display: flex; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f991bc74f..68d346859 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -20,6 +20,23 @@ = stylesheet_pack_tag 'application', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + + = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/media_attachments', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/search', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags -- cgit From 8fecd8010801c17d0d086fbb27d4d9a67ccbb6af Mon Sep 17 00:00:00 2001 From: Sylvhem Date: Sat, 8 Jul 2017 01:27:22 +0200 Subject: Various fixes in the French translation (#4107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changement de « Changement de mot de passe » en « Sécurité » * Suppression de « (Two-factor auth) » Change la valeur de la chaîne « two_factor_authentication » de « Identification à deux facteurs (Two-factor auth) » à « Identification à deux facteurs ». La traduction anglaise entre parathentèse était redondante et gênait la lecture. Change the value of the "two_factor_authentication" from "Identification à deux facteurs (Two-factor auth)" to "Identification à deux facteurs". The English translation in brackets was superflous and was getting in the way of the reader. * Remplace « ' » par « ’ » Retire de la traduction les apostrophes droites « ' » (U+0027) au profit des apostrophes typographiques « ’ » (U+2019). En typographie française, les apostrophes typographiques sont utilisées à la place des apostrophes droites. La traduction était jusqu’ici incohérente et utilisait les deux. Remove from the translation all the vertical apostrophes (U+0027) in favor of the curly ones (U+2019). In French typography, typographic apostrophes are used instead of vertical ones. The translation was incoherent and used both. * Remplace « ... » par « … » Remplace les séries de trois points par le caractère dédié « … » (U+2026). Replace all the series of three dots by the dedicated character "…" (U+2026). * Mise à jour Crée config/locales/activerecord.fr.yml, ajoute de nouvelles chaînes et met à jour certains textes. Les compteurs de caractères pour le pseudonyme et la biographie devrait maintenant pouvoir fonctionner même quand l’interface est en français. Create config/locales/activerecord.fr.yml, add new strings et update some textes. The caracters counters for the username and the biography should now work even when the interface is in French. * Remplace « A » par « À » Remplace « A » par « À » aux endroits où le mot est mal orthographié. Replace "A" by "À" when the wrong word is used. * Ajout d’espaces insécables Ajoute des espaces insécables suivant les régles nécessaires en typographie française. Add non-breaking spaces following rules of French typography. * Remplace « certain » par « certain·e » Harmonise la traduction en remplaçant « certain » par sa forme épicène. Harmonize the translation by replacing "certain" (sure) by its epicene form. * Corrige un angliscisme Remplace « adresse e-mail » par « adresse électronique ». Replace "adresse e-mail" (e-mail address) by "adresse électronique" (electronic address). --- app/javascript/mastodon/locales/fr.json | 46 ++++---- .../confirmation_instructions.fr.html.erb | 4 +- .../confirmation_instructions.fr.text.erb | 6 +- .../reset_password_instructions.fr.html.erb | 6 +- .../reset_password_instructions.fr.text.erb | 6 +- config/locales/activerecord.fr.yml | 12 ++ config/locales/devise.fr.yml | 16 +-- config/locales/doorkeeper.fr.yml | 54 ++++----- config/locales/fr.yml | 130 ++++++++++----------- config/locales/simple_form.fr.yml | 24 ++-- 10 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 config/locales/activerecord.fr.yml (limited to 'app/views') diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index fd2b30444..cb7e1b5a7 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -31,10 +31,10 @@ "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Paramètres", - "compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", + "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", "compose_form.lock_disclaimer.lock": "verrouillé", - "compose_form.placeholder": "Qu’avez-vous en tête ?", - "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", + "compose_form.placeholder": "Qu’avez-vous en tête ?", + "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", "compose_form.publish": "Pouet ", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marquer le média comme délicat", @@ -42,13 +42,13 @@ "compose_form.spoiler_placeholder": "Avertissement", "confirmation_modal.cancel": "Annuler", "confirmations.block.confirm": "Bloquer", - "confirmations.block.message": "Confirmez vous le blocage de {name} ?", + "confirmations.block.message": "Confirmez vous le blocage de {name} ?", "confirmations.delete.confirm": "Supprimer", - "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?", + "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.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.mute.message": "Confirmez vous la silenciation {name} ?", "emoji_button.activity": "Activités", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Boire et manger", @@ -59,20 +59,20 @@ "emoji_button.search": "Recherche…", "emoji_button.symbols": "Symboles", "emoji_button.travel": "Lieux et voyages", - "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", + "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag", "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.", "empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.", "empty_column.home.public_timeline": "le fil public", "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.", - "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.", + "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.", "follow_request.authorize": "Autoriser", "follow_request.reject": "Rejeter", "getting_started.appsshort": "Applications", "getting_started.faq": "FAQ", "getting_started.heading": "Pour commencer", "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", - "getting_started.userguide": "Guide d'utilisation", + "getting_started.userguide": "Guide d’utilisation", "home.column_settings.advanced": "Avancé", "home.column_settings.basic": "Basique", "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", @@ -93,17 +93,17 @@ "navigation_bar.mutes": "Comptes silencés", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", - "notification.favourite": "{name} a ajouté à ses favoris :", + "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", - "notification.mention": "{name} vous a mentionné⋅e :", - "notification.reblog": "{name} a partagé votre statut :", + "notification.mention": "{name} vous a mentionné⋅e :", + "notification.reblog": "{name} a partagé votre statut :", "notifications.clear": "Nettoyer", - "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", + "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.column_settings.alert": "Notifications locales", - "notifications.column_settings.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", - "notifications.column_settings.mention": "Mentions :", - "notifications.column_settings.reblog": "Partages :", + "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", + "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", "onboarding.done": "Effectué", @@ -112,18 +112,18 @@ "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez", "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous", "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", - "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d'utilisateur⋅ice complet est {handle}", - "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", + "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}", + "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}", "onboarding.page_six.almost_done": "Nous y sommes presque…", "onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!", "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", "onboarding.page_six.guidelines": "règles de la communauté", - "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", + "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", "onboarding.page_six.various_app": "applications mobiles", "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.", - "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d'utilisateur⋅ice complet.", + "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.", "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", "onboarding.skip": "Passer", "privacy.change": "Ajuster la confidentialité du message", @@ -151,7 +151,7 @@ "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", "status.reblog": "Partager", - "status.reblogged_by": "{name} a partagé :", + "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", "status.replyAll": "Répondre au fil", "status.report": "Signaler @{name}", diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb index b0b3d0f51..fe3f0a010 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb @@ -5,10 +5,10 @@

Pour confirmer votre inscription, merci de cliquer sur le lien suivant :
<%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %>

-

Après votre première connexion, vous pourrez accéder à la documentation de l'outil.

+

Après votre première connexion, vous pourrez accéder à la documentation de l’outil.

Pensez également à jeter un œil à nos <%= link_to 'conditions d\'utilisation', terms_url %>.

Amicalement,

-

L'équipe <%= @instance %>

\ No newline at end of file +

L’équipe <%= @instance %>

diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb index cf8e39689..7730715f8 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb @@ -5,10 +5,10 @@ Vous venez de vous créer un compte sur <%= @instance %> et nous vous en remerci Pour confirmer votre inscription, merci de cliquer sur le lien suivant : <%= confirmation_url(@resource, confirmation_token: @token) %> -Après votre première connexion, vous pourrez accéder à la documentation de l'outil. +Après votre première connexion, vous pourrez accéder à la documentation de l’outil. -Pour rappel, nos conditions d'utilisation sont indiquées ici <%= terms_url %> +Pour rappel, nos conditions d’utilisation sont indiquées ici <%= terms_url %> Amicalement, -L'équipe <%= @instance %> \ No newline at end of file +L’équipe <%= @instance %> diff --git a/app/views/user_mailer/reset_password_instructions.fr.html.erb b/app/views/user_mailer/reset_password_instructions.fr.html.erb index 95789e387..db55c5884 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.html.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.html.erb @@ -1,8 +1,8 @@

Bonjour <%= @resource.email %> !

-

Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.

+

Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.

<%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %>

-

Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.

-

Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.

+

Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message.

+

Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau.

diff --git a/app/views/user_mailer/reset_password_instructions.fr.text.erb b/app/views/user_mailer/reset_password_instructions.fr.text.erb index 73160cb4c..07fa3644a 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.text.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.text.erb @@ -1,8 +1,8 @@ Bonjour <%= @resource.email %> ! -Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. +Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. <%= edit_password_url(@resource, reset_password_token: @token) %> -Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. -Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau. +Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message. +Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau. diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml new file mode 100644 index 000000000..858777c0e --- /dev/null +++ b/config/locales/activerecord.fr.yml @@ -0,0 +1,12 @@ +fr: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: seulement des lettres, des nombres et des tirets bas + status: + attributes: + reblog: + taken: du statut existe déjà diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index c4dbc62e0..6805e4f38 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -3,8 +3,8 @@ fr: devise: confirmations: confirmed: Votre compte a été validé. - send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. - send_paranoid_instructions: Si votre adresse e-mail existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. + send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre adresse électronique existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. failure: already_authenticated: Vous êtes déjà connecté⋅e inactive: Votre compte n’est pas encore activé. @@ -25,12 +25,12 @@ fr: unlock_instructions: subject: Instructions pour déverrouiller votre compte omniauth_callbacks: - failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' + failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' success: Authentifié avec succès via %{kind}. passwords: - no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d'utiliser l'URL complète. - send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants - send_paranoid_instructions: Si votre addresse e-mail existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel + no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d’utiliser l’URL complète. + send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre addresse électronique existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. updated: Votre mot de passe a été modifié avec succès, vous êtes maintenant connecté⋅e updated_not_active: Votre mot de passe a été modifié avec succès. registrations: @@ -46,8 +46,8 @@ fr: signed_in: Connecté. signed_out: Déconnecté. unlocks: - send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants - send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. + send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e. errors: messages: diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index 24538bc48..0e74532c1 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -6,12 +6,12 @@ fr: remote_follow: attributes: acct: - blank: Le nom d'utilisateur ne doit pas être vide + blank: Le nom d’utilisateur ne doit pas être vide activerecord: attributes: doorkeeper/application: name: Nom - redirect_uri: L'URL de redirection + redirect_uri: L’URL de redirection errors: messages: record_invalid: Données invalides @@ -50,17 +50,17 @@ fr: edit: Modifier submit: Envoyer confirmations: - destroy: Êtes-vous certain ? + destroy: Êtes-vous certain·e ? edit: - title: Modifier l'application + title: Modifier l’application form: - error: Oups ! Vérifier votre formulaire pour des erreurs possibles + error: Oups ! Vérifier votre formulaire pour des erreurs possibles help: native_redirect_uri: Utiliser %{native_redirect_uri} pour les tests locaux redirect_uri: Utiliser une ligne par URL scopes: Séparer les portées avec des espaces. Laisser vide pour utiliser les portées par défaut. index: - callback_url: URL de retour d'appel + callback_url: URL de retour d’appel name: Nom new: Nouvelle application title: Vos applications @@ -68,11 +68,11 @@ fr: title: Nouvelle application show: actions: Actions - application_id: ID de l'application - callback_urls: URL du retour d'appel + application_id: ID de l’application + callback_urls: URL du retour d’appel scopes: Portées secret: Secret - title: 'Application : %{name}' + title: 'Application : %{name}' authorizations: buttons: authorize: Autoriser @@ -81,15 +81,15 @@ fr: title: Une erreur est survenue new: able_to: Cette application pourra - prompt: Autoriser %{client_name} à utiliser votre compte ? + prompt: Autoriser %{client_name} à utiliser votre compte ? title: Autorisation requise show: - title: Code d'autorisation + title: Code d’autorisation authorized_applications: buttons: revoke: Annuler confirmations: - revoke: Êtes-vous certain ? + revoke: Êtes-vous certain·e ? index: application: Application created_at: Créé le @@ -98,24 +98,24 @@ fr: title: Vos applications autorisées errors: messages: - access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la requête. - credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré. - invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse ou d'une méthode d'authentification non prise en charge. - invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la requête d'autorisation ou a été émis à un autre client. - invalid_redirect_uri: L'URL de redirection n'est pas valide. + access_denied: Le propriétaire de la ressource ou le serveur d’autorisation a refusé la requête. + credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n’est pas configuré. + invalid_client: L’authentification du client a échoué à cause d’un client inconnu, d’aucune authentification de client incluse ou d’une méthode d’authentification non prise en charge. + invalid_grant: Le consentement d’autorisation accordé n’est pas valide, a expiré, est annulé, ne concorde pas avec l’URL de redirection utilisée dans la requête d’autorisation ou a été émis à un autre client. + invalid_redirect_uri: L’URL de redirection n’est pas valide. invalid_request: La requête omet un paramètre requis, inclut une valeur de paramètre non prise en charge ou est autrement mal formée. invalid_resource_owner: Les identifiants fournis par le propriétaire de la ressource ne sont pas valides ou le propriétaire de la ressource ne peut être trouvé - invalid_scope: La portée demandée n'est pas valide, est inconnue ou mal formée. + invalid_scope: La portée demandée n’est pas valide, est inconnue ou mal formée. invalid_token: - expired: Le jeton d'accès a expiré - revoked: Le jeton d'accès a été révoqué - unknown: Le jeton d'accès n'est pas valide - resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n'est pas configuré. - server_error: Le serveur d'autorisation a rencontré une condition inattendue l'empêchant de faire aboutir la requête. - temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la requête à cause d'une surcharge ou d'une maintenance temporaire du serveur. - unauthorized_client: Le client n'est pas autorisé à effectuer cette requête à l'aide de cette méthode. - unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation. - unsupported_response_type: Le serveur d'autorisation ne prend pas en charge ce type de réponse. + expired: Le jeton d’accès a expiré + revoked: Le jeton d’accès a été révoqué + unknown: Le jeton d’accès n’est pas valide + resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n’est pas configuré. + server_error: Le serveur d’autorisation a rencontré une condition inattendue l’empêchant de faire aboutir la requête. + temporarily_unavailable: Le serveur d’autorisation est actuellement incapable de traiter la requête à cause d’une surcharge ou d’une maintenance temporaire du serveur. + unauthorized_client: Le client n’est pas autorisé à effectuer cette requête à l’aide de cette méthode. + unsupported_grant_type: Le type de consentement d’autorisation n’est pas pris en charge par le serveur d’autorisation. + unsupported_response_type: Le serveur d’autorisation ne prend pas en charge ce type de réponse. flash: applications: create: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5a3e0c552..fcf5f6f9e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,7 +7,7 @@ fr: business_email: Courriel professionnel closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. contact: Contact - description_headline: Qu'est-ce que %{domain} ? + description_headline: Qu’est-ce que %{domain} ? domain_count_after: autres instances domain_count_before: Connectés à features: @@ -34,12 +34,12 @@ fr: follow: Suivre followers: Abonné⋅es following: Abonnements - nothing_here: Rien à voir ici ! + nothing_here: Rien à voir ici ! people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} posts: Statuts remote_follow: Suivre à distance - reserved_username: Ce nom d'utilisateur⋅ice est réservé + reserved_username: Ce nom d’utilisateur⋅ice est réservé unfollow: Ne plus suivre activitypub: activity: @@ -48,14 +48,14 @@ fr: 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} + 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 ? + are_you_sure: Êtes-vous certain⋅e ? confirm: Confirmer confirmed: Confirmé - disable_two_factor_authentication: Désactiver l'authentification à deux facteurs + disable_two_factor_authentication: Désactiver l’authentification à deux facteurs display_name: Nom affiché domain: Domaine edit: Éditer @@ -85,7 +85,7 @@ fr: perform_full_suspension: Effectuer une suspension complète profile_url: URL du profil public: Public - push_subscription_expires: Expiration de l'abonnement PuSH + push_subscription_expires: Expiration de l’abonnement PuSH redownload: Rafraîchir les avatars reset: Réinitialiser reset_password: Réinitialiser le mot de passe @@ -98,12 +98,12 @@ fr: targeted_reports: Signalements créés visant ce compte silence: Rendre muet statuses: Statuts - subscribe: S'abonner + subscribe: S’abonner title: Comptes undo_silenced: Annuler le silence undo_suspension: Annuler la suspension unsubscribe: Se désabonner - username: Nom d'utilisateur⋅ice + username: Nom d’utilisateur⋅ice web: Web domain_blocks: add_new: Ajouter @@ -112,14 +112,14 @@ fr: domain: Domaine new: create: Créer le blocage - 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. + 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 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. + 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 suspend: Suspendre @@ -167,10 +167,10 @@ fr: contact_information: email: Entrez une adresse courriel publique label: Informations de contact - username: Entrez un nom d'utilisateur⋅ice + username: Entrez un nom d’utilisateur⋅ice registrations: closed_message: - desc_html: Affiché sur la page d'accueil lorsque les inscriptions sont fermées
Vous pouvez utiliser des balises HTML + desc_html: Affiché sur la page d’accueil lorsque les inscriptions sont fermées
Vous pouvez utiliser des balises HTML title: Message de fermeture des inscriptions open: disabled: Désactivées @@ -178,10 +178,10 @@ fr: title: Inscriptions setting: Paramètre site_description: - desc_html: Affichée sous la forme d'un paragraphe sur la page d'accueil et utilisée comme balise meta.
Vous pouvez utiliser des balises HTML, en particulier <a> et <em>. + desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.
Vous pouvez utiliser des balises HTML, en particulier <a> et <em>. title: Description du site site_description_extended: - desc_html: Affichée sur la page d'informations complémentaires du site
Vous pouvez utiliser des balises HTML + desc_html: Affichée sur la page d’informations complémentaires du site
Vous pouvez utiliser des balises HTML title: Description étendue du site site_title: Titre du site title: Paramètres du site @@ -198,17 +198,17 @@ fr: body: "%{reporter} a signalé %{target}" subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: - settings: 'Changer les préférences courriel : %{link}' + settings: 'Changer les préférences courriel : %{link}' signature: Notifications de Mastodon depuis %{instance} - view: 'Voir :' + view: 'Voir :' applications: - invalid_url: L'URL fournie est invalide + invalid_url: L’URL fournie est invalide auth: - change_password: Changer de mot de passe + change_password: Sécurité delete_account: Supprimer le compte delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. - didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? - forgot_password: Mot de passe oublié ? + didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? + forgot_password: Mot de passe oublié ? login: Se connecter logout: Se déconnecter register: S’inscrire @@ -218,7 +218,7 @@ fr: authorize_follow: error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant follow: Suivre - prompt_html: 'Vous (%{self}) avez demandé à suivre :' + prompt_html: 'Vous (%{self}) avez demandé à suivre :' title: Suivre %{acct} datetime: distance_in_words: @@ -230,9 +230,9 @@ fr: almost_x_years: one: un an other: "%{count} ans" - half_a_minute: A l'instant + half_a_minute: À l’instant less_than_x_minutes: "%{count}min" - less_than_x_seconds: A l'instant + less_than_x_seconds: À l’instant over_x_years: one: un an other: "%{count} ans" @@ -241,19 +241,19 @@ fr: x_months: "%{count}mois" x_seconds: "%{count}s" deletes: - bad_password_msg: Bien essayé ! Mot de passe incorrect + bad_password_msg: Bien essayé ! Mot de passe incorrect confirm_password: Entrez votre mot de passe pour vérifier votre identité - description_html: Cela va supprimer votre compte et le désactiver de manière permanente et irréversible. Votre nom d'utilisateur⋅ice restera réservé afin d'éviter la confusion + description_html: Cela va supprimer votre compte et le désactiver de manière permanente et irréversible. Votre nom d’utilisateur⋅ice restera réservé afin d’éviter la confusion proceed: Supprimer compte success_msg: Votre compte a été supprimé avec succès - warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n'étant plus abonnés à vos publications ne mettront pas leur base de données à jour. + warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n’étant plus abonnés à vos publications ne mettront pas leur base de données à jour. warning_title: Disponibilité du contenu disséminé errors: - '403': Vous n'avez pas accès à cette page. - '404': La page que vous recherchez n'existe pas. - '410': La page que vous recherchez n'existe plus. + '403': Vous n’avez pas accès à cette page. + '404': La page que vous recherchez n’existe pas. + '410': La page que vous recherchez n’existe plus. '422': - content: Vérification de sécurité échouée. Bloquez-vous les cookies ? + content: Vérification de sécurité échouée. Bloquez-vous les cookies ? title: Vérification de sécurité échouée '429': Trop de requêtes émises dans un délai donné. noscript: Pour utiliser Mastodon, veuillez activer JavaScript @@ -265,70 +265,70 @@ fr: storage: Médias stockés followers: domain: Domaine - explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent. Vous voudrez peut-être les passer en revue et les supprimer si vous n'êtes pas sûr⋅e que votre vie privée sera respectée par l'administration ou le logiciel de ces instances. - followers_count: Nombre d'abonné⋅es + explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent. Vous voudrez peut-être les passer en revue et les supprimer si vous n’êtes pas sûr⋅e que votre vie privée sera respectée par l’administration ou le logiciel de ces instances. + followers_count: Nombre d’abonné⋅es lock_link: Rendez votre compte privé - purge: Retirer de la liste d'abonné⋅es + purge: Retirer de la liste d’abonné⋅es success: - one: Suppression des abonné⋅es venant d'un domaine en cours... - other: Suppression des abonné⋅es venant de %{count} domaines en cours... - true_privacy_html: Soyez conscient⋅es qu'une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. - unlocked_warning_html: N'importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. - unlocked_warning_title: Votre compte n'est pas privé + one: Suppression des abonné⋅es venant d’un domaine en cours… + other: Suppression des abonné⋅es venant de %{count} domaines en cours… + true_privacy_html: Soyez conscient⋅es qu’une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. + unlocked_warning_html: N’importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. + unlocked_warning_title: Votre compte n’est pas privé generic: - changes_saved_msg: Les modifications ont été enregistrées avec succès ! + changes_saved_msg: Les modifications ont été enregistrées avec succès ! powered_by: propulsé par %{link} save_changes: Enregistrer les modifications validation_errors: - one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. - other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. + one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. + other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. imports: preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers créés sur une autre instance. success: Vos données ont été importées avec succès et seront traitées en temps et en heure types: - blocking: Liste d'utilisateur⋅ice⋅s bloqué⋅es - following: Liste d'utilisateur⋅ice⋅s suivi⋅es - muting: Liste d'utilisateur⋅ice⋅s que vous faites taire + blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅es + following: Liste d’utilisateur⋅ice⋅s suivi⋅es + muting: Liste d’utilisateur⋅ice⋅s que vous faites taire upload: Importer landing_strip_html: %{name} utilise %{link_to_root_path}. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". - landing_strip_signup_html: Si ce n'est pas le cas, vous pouvez en créer un ici. + landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez en créer un ici. media_attachments: validations: images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images too_many: Impossible de joindre plus de 4 fichiers notification_mailer: digest: - body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' + body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' mention: "%{name} vous a mentionné⋅e" new_followers_summary: - one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! - other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! + one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! + other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! subject: one: "Une nouvelle notification depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" favourite: - body: "%{name} a ajouté votre post à ses favoris :" + body: "%{name} a ajouté votre post à ses favoris :" subject: "%{name} a ajouté votre post à ses favoris" follow: - body: "%{name} vous suit !" + body: "%{name} vous suit !" subject: "%{name} vous suit" follow_request: body: "%{name} a demandé à vous suivre" - subject: 'Abonné⋅es en attente : %{name}' + subject: 'Abonné⋅es en attente : %{name}' mention: - body: "%{name} vous a mentionné⋅e dans :" + body: "%{name} vous a mentionné⋅e dans :" subject: "%{name} vous a mentionné⋅e" reblog: - body: "%{name} a partagé votre statut :" + body: "%{name} a partagé votre statut :" subject: "%{name} a partagé votre statut" pagination: next: Suivant prev: Précédent remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅trice - missing_resource: L'URL de redirection n'a pas pu être trouvée + missing_resource: L’URL de redirection n’a pas pu être trouvée proceed: Continuez pour suivre - prompt: 'Vous allez suivre :' + prompt: 'Vous allez suivre :' sessions: activity: Dernière activité browser: Navigateur @@ -376,7 +376,7 @@ fr: import: Import de données preferences: Préférences settings: Réglages - two_factor_authentication: Identification à deux facteurs (Two-factor auth) + two_factor_authentication: Identification à deux facteurs statuses: open_in_web: Ouvrir sur le web over_character_limit: limite de caractères dépassée de %{max} caractères @@ -397,20 +397,20 @@ fr: default: "%d %b %Y, %H:%M" two_factor_authentication: code_hint: Entrez le code généré par votre application pour confirmer - description_html: Si vous activez l'identification à deux facteurs, vous devrez être en possession de votre téléphone afin de générer un code de connexion. + description_html: Si vous activez l’identification à deux facteurs, vous devrez être en possession de votre téléphone afin de générer un code de connexion. disable: Désactiver enable: Activer - enabled: L'authentification à deux facteurs est activée + enabled: L’authentification à deux facteurs est activée enabled_success: Identification à deux facteurs activée avec succès generate_recovery_codes: Générer les codes de récupération instructions_html: "Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. - manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :' + manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :' recovery_codes: Codes de récupération recovery_codes_regenerated: Codes de récupération régénérés avec succès - recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. + recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l’accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. setup: Installer - wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ? + wrong_code: Les codes entrés sont incorrects ! L’heure du serveur et celle de votre appareil sont-elles correctes ? users: - invalid_email: L'adresse courriel est invalide - invalid_otp_token: Le code d'authentification à deux facteurs est invalide + invalid_email: L’adresse courriel est invalide + invalid_otp_token: Le code d’authentification à deux facteurs est invalide diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 446c56947..8717a4abd 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -4,12 +4,20 @@ fr: hints: defaults: avatar: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 120x120px - display_name: 30 caractères maximum + display_name: + one: 1 caractère restant + other: %{count} caractères restants header: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 700x335px - locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s'afficheront qu'à vos abonné⋅es - note: 160 caractères maximum + locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s’afficheront qu’à vos abonné⋅es + note: + one: 1 caractère restant + other: %{count} caractères restants imports: data: Un fichier CSV généré par une autre instance de Mastodon + sessions: + otp: Entrez le code d’authentification à deux facteurs depuis votre téléphone ou utilisez un de vos codes de récupération. + user: + filtered_languages: Les langues sélectionnées seront retirées de vos fils publics. labels: defaults: avatar: Image de profil @@ -21,16 +29,18 @@ fr: email: Adresse courriel header: Image d’en-tête locale: Langue - locked: Rendre le compte privé + locked: Verrouiller le compte new_password: Nouveau mot de passe note: Présentation - otp_attempt: Code d'identification à deux facteurs + otp_attempt: Code d’identification à deux facteurs password: Mot de passe + setting_auto_play_gif: Lire automatiquement les GIFs animés setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_delete_modal: Afficher un dialogue de confirmation avant de supprimer un pouet setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité - type: Type d'import + type: Type d’import username: Identifiant interactions: must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas @@ -39,7 +49,7 @@ fr: digest: Envoyer des courriels récapitulatifs favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris follow: Envoyer un courriel lorsque quelqu’un me suit - follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre + follow_request: Envoyer un courriel lorsque quelqu’un demande à me suivre mention: Envoyer un courriel lorsque quelqu’un me mentionne reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts 'no': Non -- cgit From 864e3f8d9ca652e10a28bddbb0d0df629d2849d4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2017 14:51:05 +0200 Subject: Replace OEmbed and initial state Rabl templates with serializers (#4110) * Replace OEmbed Rabl template with serializer * Replace initial state rabl with serializer --- app/controllers/api/oembed_controller.rb | 3 +- app/controllers/home_controller.rb | 17 ++++++--- app/presenters/initial_state_presenter.rb | 5 +++ app/serializers/initial_state_serializer.rb | 39 ++++++++++++++++++++ app/serializers/oembed_serializer.rb | 56 +++++++++++++++++++++++++++++ app/views/api/oembed/show.json.rabl | 14 -------- app/views/home/index.html.haml | 2 +- app/views/home/initial_state.json.rabl | 38 -------------------- spec/controllers/home_controller_spec.rb | 41 ++++----------------- 9 files changed, 121 insertions(+), 94 deletions(-) create mode 100644 app/presenters/initial_state_presenter.rb create mode 100644 app/serializers/initial_state_serializer.rb create mode 100644 app/serializers/oembed_serializer.rb delete mode 100644 app/views/api/oembed/show.json.rabl delete mode 100644 app/views/home/initial_state.json.rabl (limited to 'app/views') diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index 6e3e34d96..f8c87dd16 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -5,8 +5,7 @@ class Api::OEmbedController < Api::BaseController def show @stream_entry = find_stream_entry.stream_entry - @width = maxwidth_or_default - @height = maxheight_or_default + render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 6209a3ae9..218da6906 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,13 +2,10 @@ class HomeController < ApplicationController before_action :authenticate_user! + before_action :set_initial_state_json def index - @body_classes = 'app-body' - @token = current_session.token - @web_settings = Web::Setting.find_by(user: current_user)&.data || {} - @admin = Account.find_local(Setting.site_contact_username) - @streaming_api_base_url = Rails.configuration.x.streaming_api_base_url + @body_classes = 'app-body' end private @@ -16,4 +13,14 @@ class HomeController < ApplicationController def authenticate_user! redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? end + + def set_initial_state_json + state = InitialStatePresenter.new(settings: Web::Setting.find_by(user: current_user)&.data || {}, + current_account: current_account, + token: current_session.token, + admin: Account.find_local(Setting.site_contact_username)) + + serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb new file mode 100644 index 000000000..75fef28a8 --- /dev/null +++ b/app/presenters/initial_state_presenter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InitialStatePresenter < ActiveModelSerializers::Model + attributes :settings, :token, :current_account, :admin +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb new file mode 100644 index 000000000..84f9e23a6 --- /dev/null +++ b/app/serializers/initial_state_serializer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class InitialStateSerializer < ActiveModel::Serializer + attributes :meta, :compose, :accounts, + :media_attachments, :settings + + def meta + { + streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, + access_token: object.token, + locale: I18n.locale, + domain: Rails.configuration.x.local_domain, + me: object.current_account.id, + admin: object.admin&.id, + boost_modal: object.current_account.user.setting_boost_modal, + delete_modal: object.current_account.user.setting_delete_modal, + auto_play_gif: object.current_account.user.setting_auto_play_gif, + system_font_ui: object.current_account.user.setting_system_font_ui, + } + end + + def compose + { + me: object.current_account.id, + default_privacy: object.current_account.user.setting_default_privacy, + } + end + + def accounts + store = {} + store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) + store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) unless object.admin.nil? + store + end + + def media_attachments + { accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES } + end +end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb new file mode 100644 index 000000000..78376d253 --- /dev/null +++ b/app/serializers/oembed_serializer.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class OEmbedSerializer < ActiveModel::Serializer + include RoutingHelper + include ActionView::Helpers::TagHelper + + attributes :type, :version, :title, :author_name, + :author_url, :provider_name, :provider_url, + :cache_age, :html, :width, :height + + def type + 'rich' + end + + def version + '1.0' + end + + def author_name + object.account.display_name.presence || object.account.username + end + + def author_url + account_url(object.account) + end + + def provider_name + Rails.configuration.x.local_domain + end + + def provider_url + root_url + end + + def cache_age + 86_400 + end + + def html + tag :iframe, + src: embed_account_stream_entry_url(object.account, object), + style: 'width: 100%; overflow: hidden', + frameborder: '0', + scrolling: 'no', + width: width, + height: height + end + + def width + instance_options[:width] + end + + def height + instance_options[:height] + end +end diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl deleted file mode 100644 index 11dcec538..000000000 --- a/app/views/api/oembed/show.json.rabl +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true -object @stream_entry - -node(:type) { 'rich' } -node(:version) { '1.0' } -node(:title, &:title) -node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name } -node(:author_url) { |entry| account_url(entry.account) } -node(:provider_name) { site_hostname } -node(:provider_url) { root_url } -node(:cache_age) { 86_400 } -node(:html) { |entry| "" } -node(:width) { @width } -node(:height) { @height } diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 33c978c89..71dcb54c6 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - %script#initial-state{ type: 'application/json' }!= json_escape(render(file: 'home/initial_state', formats: :json)) + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl deleted file mode 100644 index c428a5a1f..000000000 --- a/app/views/home/initial_state.json.rabl +++ /dev/null @@ -1,38 +0,0 @@ -object false - -node(:meta) do - { - streaming_api_base_url: @streaming_api_base_url, - access_token: @token, - locale: I18n.locale, - domain: site_hostname, - me: current_account.id, - admin: @admin.try(:id), - boost_modal: current_account.user.setting_boost_modal, - delete_modal: current_account.user.setting_delete_modal, - auto_play_gif: current_account.user.setting_auto_play_gif, - system_font_ui: current_account.user.setting_system_font_ui, - } -end - -node(:compose) do - { - me: current_account.id, - default_privacy: current_account.user.setting_default_privacy, - } -end - -node(:accounts) do - store = {} - store[current_account.id] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer) - store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) unless @admin.nil? - store -end - -node(:media_attachments) do - { - accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES - } -end - -node(:settings) { @web_settings } diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index cc1dbe5a1..d44d720b1 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -23,41 +23,14 @@ RSpec.describe HomeController, type: :controller do expect(assigns(:body_classes)).to eq 'app-body' end - it 'assigns @token' do - app = Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri) - allow(Doorkeeper.configuration).to receive(:access_token_expires_in).and_return(42) - - subject - token = Doorkeeper::AccessToken.find_by(token: assigns(:token)) - - expect(token.application).to eq app - expect(token.resource_owner_id).to eq user.id - expect(token.scopes).to eq Doorkeeper::OAuth::Scopes.from_string('read write follow') - expect(token.expires_in_seconds).to eq 42 - expect(token.use_refresh_token?).to eq false - end - - it 'assigns @web_settings for {} if not available' do - subject - expect(assigns(:web_settings)).to eq({}) - end - - it 'assigns @web_settings for Web::Setting if available' do - setting = Fabricate('Web::Setting', data: '{"home":{}}', user: user) - subject - expect(assigns(:web_settings)).to eq setting.data - end - - it 'assigns @admin' do - admin = Fabricate(:account) - Setting.site_contact_username = admin.username - subject - expect(assigns(:admin)).to eq admin - end - - it 'assigns streaming_api_base_url' do + it 'assigns @initial_state_json' do subject - expect(assigns(:streaming_api_base_url)).to eq 'ws://localhost:4000' + initial_state_json = json_str_to_hash(assigns(:initial_state_json)) + expect(initial_state_json[:meta]).to_not be_nil + expect(initial_state_json[:compose]).to_not be_nil + expect(initial_state_json[:accounts]).to_not be_nil + expect(initial_state_json[:settings]).to_not be_nil + expect(initial_state_json[:media_attachments]).to_not be_nil end end end -- cgit From 794781d1219112482e4abbc0a98683a17d170e2b Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 9 Jul 2017 01:35:08 +0900 Subject: Change account link to admin account link on report page (#4119) --- app/views/admin/reports/show.html.haml | 4 ++-- app/views/authorize_follows/_card.html.haml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'app/views') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 965b71180..44486cb42 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -4,11 +4,11 @@ .report-accounts .report-accounts__item %strong= t('admin.reports.reported_account') - = render 'authorize_follows/card', account: @report.target_account + = render 'authorize_follows/card', account: @report.target_account, admin: true = render 'admin/accounts/card', account: @report.target_account .report-accounts__item %strong= t('admin.reports.reported_by') - = render 'authorize_follows/card', account: @report.account + = render 'authorize_follows/card', account: @report.account, admin: true = render 'admin/accounts/card', account: @report.account %p diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index 13d9c7719..e81e292ba 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -4,7 +4,8 @@ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' %span.display-name - = link_to TagManager.instance.url_for(account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do + - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) + = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do %strong.emojify= display_name(account) %span @#{account.acct} -- cgit From 37c832cdf7a307511b27e64174ed1a3e160ec66e Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 12:16:08 +0200 Subject: refactor: Make all reducers sync (#4125) --- app/javascript/mastodon/actions/store.js | 7 ---- app/javascript/mastodon/containers/mastodon.js | 3 +- .../mastodon/features/ui/util/async-components.js | 41 ++-------------------- app/javascript/mastodon/reducers/compose.js | 4 +-- app/javascript/mastodon/reducers/index.js | 15 ++++---- .../mastodon/reducers/media_attachments.js | 4 +-- app/javascript/mastodon/store/configureStore.js | 25 ++----------- app/views/layouts/application.html.haml | 9 ----- 8 files changed, 18 insertions(+), 90 deletions(-) (limited to 'app/views') diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 08c2810ca..efdb0771a 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -16,10 +16,3 @@ export function hydrateStore(rawState) { state, }; }; - -export function hydrateStoreLazy(name, state) { - return { - type: `${STORE_HYDRATE_LAZY}-${name}`, - state, - }; -}; diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 6e79f9e4f..87ab6023c 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -23,8 +23,7 @@ const { localeData, messages } = getLocale(); addLocaleData(localeData); export const store = configureStore(); -const initialState = JSON.parse(document.getElementById('initial-state').textContent); -export const hydrateAction = hydrateStore(initialState); +const hydrateAction = hydrateStore(JSON.parse(document.getElementById('initial-state').textContent)); store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 56880dd1f..55de114b5 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -1,40 +1,13 @@ -import { store } from '../../../containers/mastodon'; -import { refreshNotifications } from '../../../actions/notifications'; -import { injectAsyncReducer } from '../../../store/configureStore'; - -// NOTE: When lazy-loading reducers, make sure to add them -// to application.html.haml (if the component is preloaded there) - export function EmojiPicker () { return import(/* webpackChunkName: "emojione_picker" */'emojione-picker'); } export function Compose () { - return Promise.all([ - import(/* webpackChunkName: "features/compose" */'../../compose'), - import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), - import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), - import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'), - ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => { - injectAsyncReducer(store, 'compose', composeReducer.default); - injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); - injectAsyncReducer(store, 'search', searchReducer.default); - - return component; - }); + return import(/* webpackChunkName: "features/compose" */'../../compose'); } export function Notifications () { - return Promise.all([ - import(/* webpackChunkName: "features/notifications" */'../../notifications'), - import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'), - ]).then(([component, notificationsReducer]) => { - injectAsyncReducer(store, 'notifications', notificationsReducer.default); - - store.dispatch(refreshNotifications()); - - return component; - }); + return import(/* webpackChunkName: "features/notifications" */'../../notifications'); } export function HomeTimeline () { @@ -110,15 +83,7 @@ export function MediaModal () { } export function OnboardingModal () { - return Promise.all([ - import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'), - import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), - import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), - ]).then(([component, composeReducer, mediaAttachmentsReducer]) => { - injectAsyncReducer(store, 'compose', composeReducer.default); - injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); - return component; - }); + return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); } export function VideoModal () { diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 09db95e2d..d0b47a85c 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -23,7 +23,7 @@ import { COMPOSE_EMOJI_INSERT, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE_LAZY } from '../actions/store'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; import uuid from '../uuid'; @@ -134,7 +134,7 @@ const privacyPreference = (a, b) => { export default function compose(state = initialState, action) { switch(action.type) { - case `${STORE_HYDRATE_LAZY}-compose`: + case STORE_HYDRATE: return clearAll(state.merge(action.state.get('compose'))); case COMPOSE_MOUNT: return state.set('mounted', true); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 79062f2f9..919345f16 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -14,6 +14,10 @@ import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; import contexts from './contexts'; +import compose from './compose'; +import search from './search'; +import media_attachments from './media_attachments'; +import notifications from './notifications'; const reducers = { timelines, @@ -31,13 +35,10 @@ const reducers = { cards, reports, contexts, + compose, + search, + media_attachments, + notifications, }; -export function createReducer(asyncReducers) { - return combineReducers({ - ...reducers, - ...asyncReducers, - }); -} - export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index d17d465aa..85bea4f0b 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,4 +1,4 @@ -import { STORE_HYDRATE_LAZY } from '../actions/store'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -7,7 +7,7 @@ const initialState = Immutable.Map({ export default function meta(state = initialState, action) { switch(action.type) { - case `${STORE_HYDRATE_LAZY}-media_attachments`: + case STORE_HYDRATE: return state.merge(action.state.get('media_attachments')); default: return state; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 0fe29f031..1376d4cba 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,36 +1,15 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import appReducer, { createReducer } from '../reducers'; -import { hydrateStoreLazy } from '../actions/store'; -import { hydrateAction } from '../containers/mastodon'; +import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; export default function configureStore() { - const store = createStore(appReducer, compose(applyMiddleware( + return createStore(appReducer, compose(applyMiddleware( thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware() ), window.devToolsExtension ? window.devToolsExtension() : f => f)); - - store.asyncReducers = { }; - - return store; }; - -export function injectAsyncReducer(store, name, asyncReducer) { - if (!store.asyncReducers[name]) { - // Keep track that we injected this reducer - store.asyncReducers[name] = asyncReducer; - - // Add the current reducer to the store - store.replaceReducer(createReducer(store.asyncReducers)); - - // The state this reducer handles defaults to its initial state (stored inside the reducer) - // But that state may be out of date because of the server-side hydration, so we replay - // the hydration action but only for this reducer (all async reducers must listen for this dynamic action) - store.dispatch(hydrateStoreLazy(name, hydrateAction.state)); - } -} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 68d346859..580d8fb4d 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -22,19 +22,10 @@ = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/media_attachments', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/search', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' -- cgit From 2b9721d1b38319d70bed98e76a0fe1d648780298 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 10 Jul 2017 21:00:32 +0900 Subject: Add setting a always mark media as sensitive (#4136) --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/reducers/compose.js | 7 +++++++ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/serializers/initial_state_serializer.rb | 1 + app/serializers/rest/credential_account_serializer.rb | 1 + app/views/settings/preferences/show.html.haml | 2 ++ config/locales/simple_form.en.yml | 1 + spec/lib/user_settings_decorator_spec.rb | 7 +++++++ 9 files changed, 29 insertions(+) (limited to 'app/views') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index a15c26031..cac5b0ba8 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -34,6 +34,7 @@ class Settings::PreferencesController < ApplicationController def user_settings_params params.require(:user).permit( :setting_default_privacy, + :setting_default_sensitive, :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index d0b47a85c..752377739 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -45,6 +45,7 @@ const initialState = Immutable.Map({ suggestions: Immutable.List(), me: null, default_privacy: 'public', + default_sensitive: false, resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, }); @@ -75,6 +76,8 @@ function clearAll(state) { }; function appendMedia(state, media) { + const prevSize = state.get('media_attachments').size; + return state.withMutations(map => { map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); @@ -82,6 +85,10 @@ function appendMedia(state, media) { map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); + + if (prevSize === 0 && state.get('default_sensitive')) { + map.set('sensitive', true); + } }); }; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 9c0cb4545..e0e92b19d 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -18,6 +18,7 @@ class UserSettingsDecorator user.settings['notification_emails'] = merged_notification_emails user.settings['interactions'] = merged_interactions user.settings['default_privacy'] = default_privacy_preference + user.settings['default_sensitive'] = default_sensitive_preference user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference @@ -36,6 +37,10 @@ class UserSettingsDecorator settings['setting_default_privacy'] end + def default_sensitive_preference + boolean_cast_setting 'setting_default_sensitive' + end + def boost_modal_preference boolean_cast_setting 'setting_boost_modal' end diff --git a/app/models/user.rb b/app/models/user.rb index e2bb3d0ed..c80115a08 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,10 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end + def setting_default_sensitive + settings.default_sensitive + end + def setting_boost_modal settings.boost_modal end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 84f9e23a6..49ff9e377 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -23,6 +23,7 @@ class InitialStateSerializer < ActiveModel::Serializer { me: object.current_account.id, default_privacy: object.current_account.user.setting_default_privacy, + default_sensitive: object.current_account.user.setting_default_sensitive, } end diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb index 094b831c9..870d8b71f 100644 --- a/app/serializers/rest/credential_account_serializer.rb +++ b/app/serializers/rest/credential_account_serializer.rb @@ -7,6 +7,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer user = object.user { privacy: user.setting_default_privacy, + sensitive: user.setting_default_sensitive, note: object.note, } end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 26fbfdf82..56a261ab6 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -24,6 +24,8 @@ = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :follow, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index d8d3b8a6f..fc5ab5ec8 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,6 +37,7 @@ en: setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy + 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 severity: Severity diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index e1ba56d97..a67487779 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -28,6 +28,13 @@ describe UserSettingsDecorator do expect(user.settings['default_privacy']).to eq 'public' end + it 'updates the user settings value for sensitive' do + values = { 'setting_default_sensitive' => '1' } + + settings.update(values) + expect(user.settings['default_sensitive']).to eq true + end + it 'updates the user settings value for boost modal' do values = { 'setting_boost_modal' => '1' } -- cgit From 7a889a8e125a03e109b225cd83b0abcbdc76d95b Mon Sep 17 00:00:00 2001 From: STJrInuyasha Date: Mon, 10 Jul 2017 09:05:06 -0700 Subject: Remote following success page (#4129) * Added a success page to remote following Includes follow-through links to web (the old redirect target) and back to the remote user's profile * Use Account.new in spec instead of a fake with only id (fixes spec) * Fabricate(:account) over Account.new * Remove self from the success text (and all HTML with it) --- app/controllers/authorize_follows_controller.rb | 2 +- app/javascript/styles/forms.scss | 9 +++++++++ app/views/authorize_follows/success.html.haml | 16 ++++++++++++++++ config/locales/en.yml | 6 ++++++ spec/controllers/authorize_follows_controller_spec.rb | 4 ++-- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 app/views/authorize_follows/success.html.haml (limited to 'app/views') diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index da4ef022a..dccd1c209 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController if @account.nil? render :error else - redirect_to web_url("accounts/#{@account.id}") + render :success end rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError render :error diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 7a181f36b..414dc4fe8 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -375,3 +375,12 @@ code { width: 50%; } } + +.post-follow-actions { + text-align: center; + color: $ui-primary-color; + + div { + margin-bottom: 4px; + } +} diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml new file mode 100644 index 000000000..f0b495689 --- /dev/null +++ b/app/views/authorize_follows/success.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('authorize_follow.title', acct: @account.acct) + +.form-container + .follow-prompt + - if @account.locked? + %h2= t('authorize_follow.follow_request') + - else + %h2= t('authorize_follow.following') + + = render 'card', account: @account + + .post-follow-actions + %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block' + %div= t('authorize_follow.post_follow.close') diff --git a/config/locales/en.yml b/config/locales/en.yml index 60e192491..8bb893d1c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -221,6 +221,12 @@ en: authorize_follow: error: Unfortunately, there was an error looking up the remote account follow: Follow + following: 'Success! You are now following:' + follow_request: 'You have sent a follow request to:' + post_follow: + web: Go to web + return: Return to the user's profile + close: Or, you can just close this window. prompt_html: 'You (%{self}) have requested to follow:' title: Follow %{acct} datetime: diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb index b801aa661..26e46a23c 100644 --- a/spec/controllers/authorize_follows_controller_spec.rb +++ b/spec/controllers/authorize_follows_controller_spec.rb @@ -94,7 +94,7 @@ describe AuthorizeFollowsController do end it 'follows account when found' do - target_account = double(id: '123') + target_account = Fabricate(:account) result_account = double(target_account: target_account) service = double allow(FollowService).to receive(:new).and_return(service) @@ -103,7 +103,7 @@ describe AuthorizeFollowsController do post :create, params: { acct: 'acct:user@hostname' } expect(service).to have_received(:call).with(account, 'user@hostname') - expect(response).to redirect_to(web_url('accounts/123')) + expect(response).to render_template(:success) end end end -- cgit From 8784bd79d0053cb15775eb078f45e6aca7775d77 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Tue, 11 Jul 2017 22:15:42 +0900 Subject: Require stylesheets in common.js (#4152) Require stylesheets in common.js because stylesheets are shared by the entry points. --- app/javascript/mastodon/main.js | 4 ---- app/javascript/packs/common.js | 5 +++++ app/views/layouts/application.html.haml | 2 +- app/views/layouts/embedded.html.haml | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) (limited to 'app/views') diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index aca64c075..d7ffa8ea6 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,9 +1,5 @@ const perf = require('./performance'); -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); - function onDomContentLoaded(callback) { if (document.readyState !== 'loading') { callback(); diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js index 9d63d8f98..a0cb91ae4 100644 --- a/app/javascript/packs/common.js +++ b/app/javascript/packs/common.js @@ -1,2 +1,7 @@ import { start } from 'rails-ujs'; + +// import default stylesheet with variables +require('font-awesome/css/font-awesome.css'); +require('mastodon-application-style'); + start(); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 580d8fb4d..ef97fb127 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -18,7 +18,7 @@ = ' - ' = title - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 5680c1ff9..4826f32f7 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -2,7 +2,7 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' -- cgit From e19eefe219c46ea9f763d0279029f03c5cf4554f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 11 Jul 2017 15:27:59 +0200 Subject: Redesign the landing page, mount public timeline on it (#4122) * Redesign the landing page, mount public timeline on it * Adjust the standalone mounted component to the lacking of router * Adjust auth layout pages to new design * Fix tests * Standalone public timeline polling every 5 seconds * Remove now obsolete translations * Add responsive design for new landing page * Address reviews * Add floating clouds behind frontpage form * Use access token from public page when available * Fix mentions and hashtags links, cursor on status content in standalone mode * Add footer link to source code * Fix errors on pages that don't embed the component, use classnames * Fix tests * Change anonymous autoPlayGif default to false * When gif autoplay is disabled, hover to play * Add option to hide the timeline preview * Slightly improve alt layout * Add elephant friend to new frontpage * Display "back to mastodon" in place of "login" when logged in on frontpage * Change polling time to 3s --- app/controllers/about_controller.rb | 13 +- app/controllers/admin/settings_controller.rb | 9 +- app/controllers/home_controller.rb | 16 +- .../fonts/montserrat/Montserrat-Medium.ttf | Bin 0 -> 192488 bytes app/javascript/images/cloud2.png | Bin 0 -> 4973 bytes app/javascript/images/cloud3.png | Bin 0 -> 5860 bytes app/javascript/images/cloud4.png | Bin 0 -> 5273 bytes app/javascript/images/elephant-fren.png | Bin 0 -> 40859 bytes app/javascript/images/logo.svg | 2 +- .../mastodon/components/dropdown_menu.js | 19 +- .../mastodon/components/media_gallery.js | 38 +- app/javascript/mastodon/components/permalink.js | 4 +- app/javascript/mastodon/components/status.js | 8 +- .../mastodon/components/status_action_bar.js | 11 +- .../mastodon/components/status_content.js | 17 +- app/javascript/mastodon/components/video_player.js | 22 +- .../mastodon/containers/timeline_container.js | 39 ++ .../features/standalone/public_timeline/index.js | 76 ++++ app/javascript/packs/public.js | 10 + app/javascript/styles/about.scss | 448 ++++++++++++++++++--- app/javascript/styles/basics.scss | 7 +- app/javascript/styles/boost.scss | 4 + app/javascript/styles/components.scss | 32 +- app/javascript/styles/containers.scss | 48 +-- app/javascript/styles/fonts/montserrat.scss | 8 + app/javascript/styles/forms.scss | 39 +- app/presenters/instance_presenter.rb | 1 + app/serializers/initial_state_serializer.rb | 35 +- app/views/about/_features.html.haml | 25 ++ app/views/about/_registration.html.haml | 20 +- app/views/about/show.html.haml | 120 +++--- app/views/admin/settings/edit.html.haml | 43 +- app/views/auth/registrations/new.html.haml | 6 +- app/views/layouts/auth.html.haml | 3 +- config/locales/ar.yml | 13 +- config/locales/bg.yml | 13 +- config/locales/ca.yml | 17 +- config/locales/de.yml | 13 - config/locales/en.yml | 43 +- config/locales/eo.yml | 23 +- config/locales/es.yml | 13 +- config/locales/fa.yml | 13 - config/locales/fi.yml | 13 +- config/locales/fr.yml | 15 +- config/locales/he.yml | 13 - config/locales/hr.yml | 13 +- config/locales/id.yml | 13 - config/locales/io.yml | 13 - config/locales/it.yml | 13 +- config/locales/ja.yml | 15 +- config/locales/ko.yml | 19 +- config/locales/nl.yml | 21 +- config/locales/no.yml | 13 - config/locales/oc.yml | 17 +- config/locales/pl.yml | 19 +- config/locales/pt-BR.yml | 13 - config/locales/pt.yml | 13 - config/locales/ru.yml | 13 - config/locales/th.yml | 13 - config/locales/tr.yml | 13 - config/locales/uk.yml | 13 - config/locales/zh-CN.yml | 13 - config/locales/zh-HK.yml | 13 - config/locales/zh-TW.yml | 13 - config/settings.yml | 1 + lib/tasks/mastodon.rake | 8 +- spec/requests/localization_spec.rb | 8 +- spec/views/about/show.html.haml_spec.rb | 9 +- 68 files changed, 956 insertions(+), 655 deletions(-) create mode 100644 app/javascript/fonts/montserrat/Montserrat-Medium.ttf create mode 100644 app/javascript/images/cloud2.png create mode 100644 app/javascript/images/cloud3.png create mode 100644 app/javascript/images/cloud4.png create mode 100644 app/javascript/images/elephant-fren.png create mode 100644 app/javascript/mastodon/containers/timeline_container.js create mode 100644 app/javascript/mastodon/features/standalone/public_timeline/index.js create mode 100644 app/views/about/_features.html.haml (limited to 'app/views') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index c0addbecc..47690e81e 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,7 +4,10 @@ class AboutController < ApplicationController before_action :set_body_classes before_action :set_instance_presenter, only: [:show, :more, :terms] - def show; end + def show + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end def more; end @@ -15,6 +18,7 @@ class AboutController < ApplicationController def new_user User.new.tap(&:build_account) end + helper_method :new_user def set_instance_presenter @@ -24,4 +28,11 @@ class AboutController < ApplicationController def set_body_classes @body_classes = 'about-body' end + + def initial_state_params + { + settings: {}, + token: current_session&.token, + } + end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index f27a1f4d4..29b590d7a 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -11,8 +11,15 @@ module Admin site_terms open_registrations closed_registrations_message + open_deletion + timeline_preview + ).freeze + + BOOLEAN_SETTINGS = %w( + open_registrations + open_deletion + timeline_preview ).freeze - BOOLEAN_SETTINGS = %w(open_registrations).freeze def edit @settings = Setting.all_as_records diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 218da6906..8a8b9ec76 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -15,12 +15,16 @@ class HomeController < ApplicationController end def set_initial_state_json - state = InitialStatePresenter.new(settings: Web::Setting.find_by(user: current_user)&.data || {}, - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username)) - - serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer) + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = serializable_resource.to_json end + + def initial_state_params + { + settings: Web::Setting.find_by(user: current_user)&.data || {}, + current_account: current_account, + token: current_session.token, + admin: Account.find_local(Setting.site_contact_username), + } + end end diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf new file mode 100644 index 000000000..88d70b89c Binary files /dev/null and b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf differ diff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png new file mode 100644 index 000000000..f325ca6de Binary files /dev/null and b/app/javascript/images/cloud2.png differ diff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png new file mode 100644 index 000000000..ab194d0b8 Binary files /dev/null and b/app/javascript/images/cloud3.png differ diff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png new file mode 100644 index 000000000..98323f5a2 Binary files /dev/null and b/app/javascript/images/cloud4.png differ diff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png new file mode 100644 index 000000000..3b64edf08 Binary files /dev/null and b/app/javascript/images/elephant-fren.png differ diff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg index c233db842..16cb3a944 100644 --- a/app/javascript/images/logo.svg +++ b/app/javascript/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 12e1b44fa..98323b069 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent { size: PropTypes.number.isRequired, direction: PropTypes.string, ariaLabel: PropTypes.string, + disabled: PropTypes.bool, }; static defaultProps = { @@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent { } render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const { expanded } = this.state; + const { icon, items, size, direction, ariaLabel, disabled } = this.props; + const { expanded } = this.state; const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right'; + const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }; + const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`; + + if (disabled) { + return ( +
+ +
+ ); + } const dropdownItems = expanded && (