From 0ea9d8164b8f5a45b0c636a654b732018f6cb1e6 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 26 Nov 2024 02:19:20 -0500 Subject: [PATCH 01/15] Remove `body_class_string` helper (#33072) --- app/controllers/application_controller.rb | 5 ----- app/helpers/application_helper.rb | 2 +- spec/helpers/application_helper_spec.rb | 20 +++++++++++++------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d493bd43bf..7a858ed059 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base helper_method :use_seamless_external_login? helper_method :sso_account_settings helper_method :limited_federation_mode? - helper_method :body_class_string helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request @@ -158,10 +157,6 @@ class ApplicationController < ActionController::Base current_user.setting_theme end - def body_class_string - @body_classes || '' - end - def respond_with_error(code) respond_to do |format| format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3d5025724f..9861ee7e8e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -143,7 +143,7 @@ module ApplicationHelper end def body_classes - output = body_class_string.split + output = [] output << content_for(:body_classes) output << "theme-#{current_theme.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 557d08e851..942cc5103c 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -5,12 +5,20 @@ require 'rails_helper' RSpec.describe ApplicationHelper do describe 'body_classes' do context 'with a body class string from a controller' do - before { helper.extend controller_helpers } + before do + user = Fabricate :user + user.settings['web.use_system_font'] = true + user.settings['web.reduce_motion'] = true + user.save - it 'uses the controller body classes in the result' do + helper.extend controller_helpers + end + + it 'uses the current theme and user settings classes in the result' do expect(helper.body_classes) - .to match(/modal-layout compose-standalone/) - .and match(/theme-default/) + .to match(/theme-default/) + .and match(/system-font/) + .and match(/reduce-motion/) end it 'includes values set via content_for' do @@ -24,10 +32,8 @@ RSpec.describe ApplicationHelper do def controller_helpers Module.new do - def body_class_string = 'modal-layout compose-standalone' - def current_account - @current_account ||= Fabricate(:account) + @current_account ||= Fabricate(:account, user: User.last) end def current_theme = 'default' From 3e901d108c5a73212e1f2a648456f1b12a3c32ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:19:35 +0100 Subject: [PATCH 02/15] Update dependency selenium-webdriver to v4.27.0 (#33071) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e87224236b..cfda83d324 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -752,7 +752,7 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) securerandom (0.3.2) - selenium-webdriver (4.26.0) + selenium-webdriver (4.27.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) From 36496f4d73f30cfa0a22706b574c8ca57b4f0c4a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:19:48 +0100 Subject: [PATCH 03/15] Update Yarn to v4.5.3 (#33069) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- streaming/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4eb95d4389..e9f6364026 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/mastodon", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.5.2", + "packageManager": "yarn@4.5.3", "engines": { "node": ">=18" }, diff --git a/streaming/package.json b/streaming/package.json index 521544f42b..e40d262378 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/streaming", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.5.2", + "packageManager": "yarn@4.5.3", "engines": { "node": ">=18" }, From 6b1dd1bf2a7ac4658a06b5846cf9107f4462f004 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:24:28 +0100 Subject: [PATCH 04/15] New Crowdin Translations (automated) (#33074) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/zh-CN.json | 34 ++++++++--------- config/locales/bg.yml | 6 +++ config/locales/de.yml | 18 ++++----- config/locales/doorkeeper.zh-CN.yml | 6 +-- config/locales/es-AR.yml | 14 +++---- config/locales/simple_form.de.yml | 2 +- config/locales/zh-CN.yml | 44 +++++++++++----------- 7 files changed, 65 insertions(+), 59 deletions(-) diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 4046225164..6d86ed477e 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -26,8 +26,8 @@ "account.domain_blocked": "域名已屏蔽", "account.edit_profile": "修改个人资料", "account.enable_notifications": "当 @{name} 发布嘟文时通知我", - "account.endorse": "在个人资料中推荐此用户", - "account.featured_tags.last_status_at": "最近发言于 {date}", + "account.endorse": "在账户页推荐此用户", + "account.featured_tags.last_status_at": "上次发言于 {date}", "account.featured_tags.last_status_never": "暂无嘟文", "account.featured_tags.title": "{name} 的精选标签", "account.follow": "关注", @@ -105,7 +105,7 @@ "annual_report.summary.new_posts.new_posts": "发嘟", "annual_report.summary.percentile.text": "这使你跻身 Mastodon 用户的前", "annual_report.summary.percentile.we_wont_tell_bernie": "我们打死也不会告诉扣税国王的(他知道的话要来收你发嘟税了)。", - "annual_report.summary.thanks": "感谢你这一年与 Mastodon 一路同行!", + "annual_report.summary.thanks": "感谢你这一年和 Mastodon 上的大家一起嘟嘟!", "attachments_list.unprocessed": "(未处理)", "audio.hide": "隐藏音频", "block_modal.remote_users_caveat": "我们将要求服务器 {domain} 尊重你的决定。然而,我们无法保证对方一定遵从,因为某些服务器可能会以不同的方案处理屏蔽操作。公开嘟文仍然可能对未登录的用户可见。", @@ -138,7 +138,7 @@ "closed_registrations_modal.title": "注册 Mastodon 账号", "column.about": "关于", "column.blocks": "屏蔽的用户", - "column.bookmarks": "书签", + "column.bookmarks": "收藏夹", "column.community": "本站时间线", "column.create_list": "创建列表", "column.direct": "私下提及", @@ -510,7 +510,7 @@ "navigation_bar.administration": "管理", "navigation_bar.advanced_interface": "在高级网页界面中打开", "navigation_bar.blocks": "已屏蔽的用户", - "navigation_bar.bookmarks": "书签", + "navigation_bar.bookmarks": "收藏夹", "navigation_bar.community_timeline": "本站时间线", "navigation_bar.compose": "撰写新嘟文", "navigation_bar.direct": "私下提及", @@ -555,7 +555,7 @@ "notification.label.reply": "回复", "notification.mention": "提及", "notification.mentioned_you": "{name} 提到了你", - "notification.moderation-warning.learn_more": "了解更多", + "notification.moderation-warning.learn_more": "详细了解", "notification.moderation_warning": "你收到了一条管理警告", "notification.moderation_warning.action_delete_statuses": "你的一些嘟文已被移除。", "notification.moderation_warning.action_disable": "你的账号已被禁用。", @@ -571,7 +571,7 @@ "notification.relationships_severance_event": "与 {name} 的联系已断开", "notification.relationships_severance_event.account_suspension": "{from} 的管理员封禁了 {target},这意味着你将无法再收到对方的更新或与其互动。", "notification.relationships_severance_event.domain_block": "{from} 的管理员屏蔽了 {target},其中包括你的 {followersCount} 个关注者和 {followingCount, plural, other {# 个关注}}。", - "notification.relationships_severance_event.learn_more": "了解更多", + "notification.relationships_severance_event.learn_more": "详细了解", "notification.relationships_severance_event.user_domain_block": "你已经屏蔽了 {target},移除了你的 {followersCount} 个关注者和 {followingCount, plural, other {# 个关注}}。", "notification.status": "{name} 刚刚发布嘟文", "notification.update": "{name} 编辑了嘟文", @@ -717,11 +717,11 @@ "regeneration_indicator.label": "加载中…", "regeneration_indicator.sublabel": "你的主页动态正在准备中!", "relative_time.days": "{number} 天前", - "relative_time.full.days": "{number, plural, one {# 天} other {# 天}}前", - "relative_time.full.hours": "{number, plural, one {# 小时} other {# 小时}}前", + "relative_time.full.days": "{number, plural, other {# 天}}前", + "relative_time.full.hours": "{number, plural, other {# 小时}}前", "relative_time.full.just_now": "刚刚", - "relative_time.full.minutes": "{number, plural, one {# 分钟} other {# 分钟}}前", - "relative_time.full.seconds": "{number, plural, one {# 秒} other {# 秒}}前", + "relative_time.full.minutes": "{number, plural, other {# 分钟}}前", + "relative_time.full.seconds": "{number, plural, other {# 秒}}前", "relative_time.hours": "{number} 小时前", "relative_time.just_now": "刚刚", "relative_time.minutes": "{number} 分钟前", @@ -817,7 +817,7 @@ "status.admin_domain": "打开 {domain} 的管理界面", "status.admin_status": "打开此帖的管理界面", "status.block": "屏蔽 @{name}", - "status.bookmark": "添加到书签", + "status.bookmark": "收藏", "status.cancel_reblog_private": "取消转贴", "status.cannot_reblog": "这条嘟文不允许被转嘟", "status.continued_thread": "上接嘟文串", @@ -853,7 +853,7 @@ "status.reblogs": "{count, plural, other {次转嘟}}", "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。", "status.redraft": "删除并重新编辑", - "status.remove_bookmark": "移除书签", + "status.remove_bookmark": "取消收藏", "status.replied_in_thread": "回复给嘟文串", "status.replied_to": "回复给 {name}", "status.reply": "回复", @@ -875,11 +875,11 @@ "subscribed_languages.target": "更改 {target} 的订阅语言", "tabs_bar.home": "主页", "tabs_bar.notifications": "通知", - "time_remaining.days": "剩余 {number, plural, one {# 天} other {# 天}}", - "time_remaining.hours": "剩余 {number, plural, one {# 小时} other {# 小时}}", - "time_remaining.minutes": "剩余 {number, plural, one {# 分钟} other {# 分钟}}", + "time_remaining.days": "剩余 {number, plural, other {# 天}}", + "time_remaining.hours": "剩余 {number, plural, other {# 小时}}", + "time_remaining.minutes": "剩余 {number, plural, other {# 分钟}}", "time_remaining.moments": "即将结束", - "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}", + "time_remaining.seconds": "剩余 {number, plural, other {# 秒}}", "trends.counter_by_accounts": "过去 {days, plural, other {{days} 天}}有{count, plural, other { {counter} 人}}讨论", "trends.trending_now": "当前热门", "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会丢失。", diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 20ee1f6c68..a51ef24262 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -226,20 +226,26 @@ bg: approve_appeal_html: "%{name} одобри обжалването на решение за модериране от %{target}" approve_user_html: "%{name} одобри регистрирането от %{target}" assigned_to_self_report_html: "%{name} възложи на себе си доклад %{target}" + change_email_user_html: "%{name} промени адреса на имейла на потребителя %{target}" change_role_user_html: "%{name} промени ролята на %{target}" + confirm_user_html: "%{name} потвърди адреса на имейла на потребителя %{target}" create_account_warning_html: "%{name} изпрати предупреждение до %{target}" create_announcement_html: "%{name} създаде ново оповестяване %{target}" + create_canonical_email_block_html: "%{name} блокира имейл с хеш %{target}" create_custom_emoji_html: "%{name} качи ново емоджи %{target}" create_domain_allow_html: "%{name} позволи федерирането с домейн %{target}" create_domain_block_html: "%{name} блокира домейн %{target}" + create_email_domain_block_html: "%{name} блокира домейн за е-поща %{target}" create_ip_block_html: "%{name} създаде правило за IP %{target}" create_unavailable_domain_html: "%{name} спря доставянето до домейн %{target}" create_user_role_html: "%{name} създаде роля %{target}" demote_user_html: "%{name} понижи потребителя %{target}" destroy_announcement_html: "%{name} изтри оповестяване %{target}" + destroy_canonical_email_block_html: "%{name} отблокира имейла с хеш %{target}" destroy_custom_emoji_html: "%{name} изтри емоджито %{target}" destroy_domain_allow_html: "%{name} забрани федерирация с домейн %{target}" destroy_domain_block_html: "%{name} отблокира домейн %{target}" + destroy_email_domain_block_html: "%{name} отблокира домейн за е-поща %{target}" destroy_instance_html: "%{name} прочисти домейн %{target}" destroy_ip_block_html: "%{name} изтри правило за IP %{target}" destroy_status_html: "%{name} премахна публикация от %{target}" diff --git a/config/locales/de.yml b/config/locales/de.yml index 26ccc8f3ee..1c82b4da3f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1180,12 +1180,12 @@ de: use_security_key: Sicherheitsschlüssel verwenden author_attribution: example_title: Beispieltext - hint_html: Schreibst du außerhalb von Mastodon Nachrichtenartikel oder betreibst du einen Blog? Bestimme, wie du Anerkennungen durch geteilte Links auf Mastodon handhaben möchtest. - instructions: 'Der nachfolgende Code muss im HTML-Code deines Artikels sein:' + hint_html: Schreibst du außerhalb von Mastodon journalistische Artikel oder andere Texte, beispielsweise in einem Blog? Lege hier fest, wann auf dein Profil verwiesen werden soll, wenn Links zu deinen Werken auf Mastodon geteilt werden. + instructions: 'Der nachfolgende Code muss im HTML-Header deines zu verlinkenden Textes stehen:' more_from_html: Mehr von %{name} s_blog: Blog von %{name} - then_instructions: Ergänze die Domain, auf der deine Inhalte veröffentlicht werden in das unten stehende Feld. - title: Anerkennung als Autor*in + then_instructions: Ergänze anschließend im unteren Feld die Domain, auf der sich deine Inhalte befinden. + title: Verifizierung als Autor*in challenge: confirm: Fortfahren hint_html: "Hinweis: Wir werden dich für die nächste Stunde nicht erneut nach deinem Passwort fragen." @@ -1625,7 +1625,7 @@ de: posting_defaults: Standardeinstellungen für Beiträge public_timelines: Öffentliche Timelines privacy: - hint_html: "Bestimme, wie dein Profil und deine Beiträge gefunden werden sollen. Eine Vielzahl von Funktionen in Mastodon können dir helfen, eine größere Reichweite zu erlangen, wenn sie aktiviert sind. Nimm dir einen Moment Zeit, um diese Einstellungen zu überprüfen und sicherzustellen, dass sie für deinen Anwendungsfall geeignet sind." + hint_html: "Bestimme selbst, wie dein Profil und deine Beiträge gefunden werden sollen. Zahlreiche Mastodon-Funktionen können dir für eine größere Reichweite behilflich sein. Nimm dir einen Moment Zeit, um diese Einstellungen zu überprüfen." privacy: Datenschutz privacy_hint_html: Bestimme, wie viele Informationen du für andere preisgeben möchtest. Viele Menschen entdecken interessante Profile und coole Apps, indem sie die Follower anderer Profile durchstöbern und die Apps sehen, über die Beiträge veröffentlicht wurden – möglicherweise möchtest du diese Informationen ausblenden. reach: Reichweite @@ -1801,7 +1801,7 @@ de: enabled: Alte Beiträge automatisch entfernen enabled_hint: Löscht automatisch deine Beiträge, sobald sie die angegebene Altersgrenze erreicht haben, es sei denn, sie entsprechen einer der unten angegebenen Ausnahmen exceptions: Ausnahmen - explanation: Damit Mastodon nicht durch das Löschen von Beiträgen ausgebremst wird, wartet der Server damit, bis wenig los ist. Aus diesem Grund werden deine Beiträge ggf. erst einige Zeit nach Erreichen der Altersgrenze gelöscht. + explanation: Damit der Server nicht durch das Löschen von Beiträgen ausgebremst wird, wartet die Mastodon-Software, bis wenig(er) los ist. Deshalb könnten deine Beiträge ggf. erst einige Zeit nach Erreichen der Altersgrenze gelöscht werden. ignore_favs: Favoriten ignorieren ignore_reblogs: Geteilte Beiträge ignorieren interaction_exceptions: Ausnahmen basierend auf Interaktionen @@ -1980,13 +1980,13 @@ de: seamless_external_login: Du bist über einen externen Dienst angemeldet, daher sind Passwort- und E-Mail-Einstellungen nicht verfügbar. signed_in_as: 'Angemeldet als:' verification: - extra_instructions_html: Hinweis: Der Link auf deiner Website kann unsichtbar sein. Der wichtige Teil ist rel="me", wodurch das Nachahmen von Personen auf Websites mit nutzergenerierten Inhalten verhindert wird. Du kannst auch ein link-Tag statt a im Header auf der Seite verwenden, jedoch muss der HTML-Code ohne das Ausführen von JavaScript zugänglich sein. + extra_instructions_html: Hinweis: Der Link auf deiner Website kann unsichtbar sein. Der wichtige Teil ist rel="me". Du kannst auch den Tag link im head (statt a im body) verwenden, jedoch muss die Internetseite ohne JavaScript abrufbar sein. here_is_how: So funktioniert’s hint_html: "Alle können ihre Identität auf Mastodon verifizieren. Basierend auf offenen Standards – jetzt und für immer kostenlos. Alles, was du brauchst, ist eine eigene Website. Wenn du von deinem Profil auf diese Website verlinkst, überprüfen wir, ob die Website zu deinem Profil zurückverlinkt, und zeigen einen visuellen Hinweis an." instructions_html: Kopiere den unten stehenden Code und füge ihn in den HTML-Code deiner Website ein. Trage anschließend die Adresse deiner Website in ein Zusatzfeld auf deinem Profil ein und speichere die Änderungen. Die Zusatzfelder befinden sich im Reiter „Profil bearbeiten“. verification: Verifizierung - verified_links: Deine verifizierten Links - website_verification: Website-Verifizierung + verified_links: Deine verifizierten Domains + website_verification: Verifizierung einer Website webauthn_credentials: add: Sicherheitsschlüssel hinzufügen create: diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index d14bd575f4..50705932e6 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -125,7 +125,7 @@ zh-CN: admin/reports: 举报管理 all: 完全访问你的Mastodon账户 blocks: 屏蔽 - bookmarks: 书签 + bookmarks: 收藏 conversations: 会话 crypto: 端到端加密 favourites: 喜欢 @@ -172,7 +172,7 @@ zh-CN: read: 读取你的账户数据 read:accounts: 查看账号信息 read:blocks: 查看你的屏蔽列表 - read:bookmarks: 查看你的书签 + read:bookmarks: 查看你的收藏夹 read:favourites: 查看喜欢的嘟文 read:filters: 查看你的过滤规则 read:follows: 查看你的关注 @@ -185,7 +185,7 @@ zh-CN: write: 修改你的账号数据 write:accounts: 修改你的个人资料 write:blocks: 屏蔽账号和域名 - write:bookmarks: 为嘟文添加书签 + write:bookmarks: 收藏嘟文 write:conversations: 静音并删除会话 write:favourites: 喜欢嘟文 write:filters: 创建过滤规则 diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 09163c2e4a..170c16f094 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -187,7 +187,7 @@ es-AR: create_domain_block: Crear bloqueo de dominio create_email_domain_block: Crear bloqueo de dominio de correo electrónico create_ip_block: Crear regla de dirección IP - create_relay: Crear Relé + create_relay: Crear relé create_unavailable_domain: Crear dominio no disponible create_user_role: Crear rol demote_user: Descender usuario @@ -199,17 +199,17 @@ es-AR: destroy_email_domain_block: Eliminar bloqueo de dominio de correo electrónico destroy_instance: Purgar dominio destroy_ip_block: Eliminar regla de dirección IP - destroy_relay: Eliminar Relé + destroy_relay: Eliminar relé destroy_status: Eliminar mensaje destroy_unavailable_domain: Eliminar dominio no disponible destroy_user_role: Destruir rol disable_2fa_user: Deshabilitar 2FA disable_custom_emoji: Deshabilitar emoji personalizado - disable_relay: Desactivar Relé + disable_relay: Deshabilitar relé disable_sign_in_token_auth_user: Deshabilitar autenticación de token por correo electrónico para el usuario disable_user: Deshabilitar usuario enable_custom_emoji: Habilitar emoji personalizado - enable_relay: Activar Relé + enable_relay: Habilitar relé enable_sign_in_token_auth_user: Habilitar autenticación de token por correo electrónico para el usuario enable_user: Habilitar usuario memorialize_account: Convertir en cuenta conmemorativa @@ -251,7 +251,7 @@ es-AR: create_domain_block_html: "%{name} bloqueó el dominio %{target}" create_email_domain_block_html: "%{name} bloqueó el dominio de correo electrónico %{target}" create_ip_block_html: "%{name} creó la regla para la dirección IP %{target}" - create_relay_html: "%{name} creó un relé %{target}" + create_relay_html: "%{name} creó el relé %{target}" create_unavailable_domain_html: "%{name} detuvo la entrega al dominio %{target}" create_user_role_html: "%{name} creó el rol %{target}" demote_user_html: "%{name} bajó de nivel al usuario %{target}" @@ -269,11 +269,11 @@ es-AR: destroy_user_role_html: "%{name} eliminó el rol %{target}" disable_2fa_user_html: "%{name} deshabilitó el requerimiento de dos factores para el usuario %{target}" disable_custom_emoji_html: "%{name} deshabilitó el emoji %{target}" - disable_relay_html: "%{name} desactivó el relé %{target}" + disable_relay_html: "%{name} deshabilitó el relé %{target}" disable_sign_in_token_auth_user_html: "%{name} deshabilitó la autenticación de token por correo electrónico para %{target}" disable_user_html: "%{name} deshabilitó el inicio de sesión para el usuario %{target}" enable_custom_emoji_html: "%{name} habilitó el emoji %{target}" - enable_relay_html: "%{name} activó el relé %{target}" + enable_relay_html: "%{name} eliminó el relé %{target}" enable_sign_in_token_auth_user_html: "%{name} habilitó la autenticación de token por correo electrónico para %{target}" enable_user_html: "%{name} habilitó el inicio de sesión para el usuario %{target}" memorialize_account_html: "%{name} convirtió la cuenta de %{target} en una cuenta conmemorativa" diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 08d5331151..d6d6536737 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -144,7 +144,7 @@ de: url: Wohin Ereignisse gesendet werden labels: account: - attribution_domains_as_text: Websites, die dich anerkennen dürfen + attribution_domains_as_text: Websites, die auf dich verweisen dürfen discoverable: Profil und Beiträge in Suchalgorithmen berücksichtigen fields: name: Beschriftung diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index d2170d3e11..a87f8e64b0 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1272,7 +1272,7 @@ zh-CN: request: 请求你的存档 size: 大小 blocks: 屏蔽的用户 - bookmarks: 书签 + bookmarks: 收藏 csv: CSV domain_blocks: 域名屏蔽 lists: 列表 @@ -1361,7 +1361,7 @@ zh-CN: blocking_html: other: 你即将使用来自 %{filename} 的最多 %{count} 个账户替换你的屏蔽列表。 bookmarks_html: - other: 你即将使用来自 %{filename} 的最多 %{count} 条嘟文替换你的书签。 + other: 你即将使用来自 %{filename} 的最多 %{count} 条嘟文替换你的收藏列表。 domain_blocking_html: other: 你即将使用来自 %{filename} 的最多 %{count} 个域名替换你的域名屏蔽列表。 following_html: @@ -1374,7 +1374,7 @@ zh-CN: blocking_html: other: 你即将屏蔽来自 %{filename} 的最多 %{count} 个账号。 bookmarks_html: - other: 你即将把来自 %{filename} %{count} 篇嘟文添加到你的书签中。 + other: 你即将把来自 %{filename} %{count} 篇嘟文添加到你的收藏夹中。 domain_blocking_html: other: 你即将屏蔽来自 %{filename} 的最多 %{count} 个域名。 following_html: @@ -1395,18 +1395,18 @@ zh-CN: time_started: 开始于 titles: blocking: 正在导入被屏蔽的账户 - bookmarks: 正在导入书签 + bookmarks: 正在导入收藏 domain_blocking: 正在导入被屏蔽的域名 following: 正在导入关注的账户 lists: 导入列表 muting: 正在导入隐藏的账户 type: 导入类型 type_groups: - constructive: 关注和书签 + constructive: 关注与收藏 destructive: 屏蔽与隐藏 types: blocking: 屏蔽列表 - bookmarks: 书签 + bookmarks: 收藏 domain_blocking: 域名屏蔽列表 following: 关注列表 lists: 列表 @@ -1757,24 +1757,24 @@ zh-CN: unlisted_long: 对所有人可见,但不出现在公共时间线上 statuses_cleanup: enabled: 自动删除旧嘟文 - enabled_hint: 达到指定过期时间后自动删除你的嘟文,除非满足下列条件之一 + enabled_hint: 自动删除你发布的超过指定期限的嘟文,除非满足下列条件之一 exceptions: 例外 - explanation: 删除嘟文是一个消耗系统资源的耗时操作,所以这个操作会在服务器空闲时完成。因此,你的嘟文可能会在达到过期阈值之后一段时间才会被删除。 - ignore_favs: 取消喜欢 - ignore_reblogs: 忽略转嘟 + explanation: 删除嘟文会占用大量服务器资源,所以这个操作将在服务器空闲时完成。因此,你的嘟文可能会在达到删除期限之后一段时间才会被删除。 + ignore_favs: 喜欢数阈值 + ignore_reblogs: 转嘟数阈值 interaction_exceptions: 基于互动的例外 - interaction_exceptions_explanation: 请注意,如果嘟文超出转嘟和喜欢的阈值之后,又降到阈值以下,则可能不会被删除。 + interaction_exceptions_explanation: 请注意,如果嘟文的转嘟数和喜欢数超过保留阈值之后,又降到阈值以下,则可能不会被删除。 keep_direct: 保留私信 - keep_direct_hint: 不会删除你的任何私信 + keep_direct_hint: 不删除你的任何私信 keep_media: 保留带媒体附件的嘟文 - keep_media_hint: 不会删除任何包含媒体附件的嘟文 + keep_media_hint: 不删除任何包含媒体附件的嘟文 keep_pinned: 保留置顶嘟文 - keep_pinned_hint: 不会删除你的任何置顶嘟文 + keep_pinned_hint: 不删除你的任何置顶嘟文 keep_polls: 保留投票 - keep_polls_hint: 不会删除你的任何投票 - keep_self_bookmark: 保存被你加入书签的嘟文 - keep_self_bookmark_hint: 如果你已将自己的嘟文添加书签,就不会删除这些嘟文 - keep_self_fav: 保留你点赞的嘟文 + keep_polls_hint: 不删除你的任何投票 + keep_self_bookmark: 保存你收藏的的嘟文 + keep_self_bookmark_hint: 不删除你收藏的嘟文 + keep_self_fav: 保留你喜欢的嘟文 keep_self_fav_hint: 如果你喜欢了自己的嘟文,则不会删除这些嘟文 min_age: '1209600': 2周 @@ -1786,8 +1786,8 @@ zh-CN: '63113904': 两年 '7889238': 3个月 min_age_label: 过期阈值 - min_favs: 保留如下嘟文:点赞数超过 - min_favs_hint: 点赞数超过该阈值的的嘟文都不会被删除。如果留空,则无论嘟文获得多少点赞,都将被删除。 + min_favs: 保留如下嘟文:喜欢数超过 + min_favs_hint: 获得喜欢数超过该阈值的的嘟文都不会被删除。如果留空,则无论嘟文获得多少点赞,都将被删除。 min_reblogs: 保留如下嘟文:转嘟数超过 min_reblogs_hint: 转嘟数超过该阈值的的嘟文不会被删除。如果留空,则无论嘟文获得多少转嘟,都将被删除。 stream_entries: @@ -1899,13 +1899,13 @@ zh-CN: edit_profile_step: 完善个人资料,提升你的互动体验。 edit_profile_title: 个性化你的个人资料 explanation: 下面是几个小贴士,希望它们能帮到你 - feature_action: 了解更多 + feature_action: 详细了解 feature_audience: Mastodon 为你提供了无需中间商即可管理受众的独特可能。Mastodon 可被部署在你自己的基础设施上,允许你关注其它任何 Mastodon 在线服务器的用户,或被任何其他在线 Mastodon 服务器的用户关注,并且不受你之外的任何人控制。 feature_audience_title: 自由吸引你的受众 feature_control: 你最清楚你想在你自己的主页中看到什么动态。没有算法或广告浪费你的时间。你可以用一个账号关注任何 Mastodon 服务器上的任何人,并按时间顺序获得他们发布的嘟文,让你的互联网的角落更合自己的心意。 feature_control_title: 掌控自己的时间线 feature_creativity: Mastodon 支持音频、视频和图片、无障碍描述、投票、内容警告, 动画头像、自定义表情包、缩略图裁剪控制等功能,帮助你在网上尽情表达自己。无论你是要发布你的艺术作品、音乐还是播客,Mastodon 都能为你服务。 - feature_creativity_title: 无与伦比的创造力 + feature_creativity_title: 尽情发挥创造力 feature_moderation: Mastodon 将决策权交还给你。每个服务器都会创建自己的规则和条例,并在站点内施行,而不是像企业社交媒体那样居高临下,这使得它可以最灵活地响应不同人群的需求。加入一个你认同其规则的服务器,或托管你自己的服务器。 feature_moderation_title: 管理,本应如此 follow_action: 关注 From 72f623c391e2f27cd25cb22d78286ac9738aa8f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:26:47 +0000 Subject: [PATCH 05/15] Update dependency @dnd-kit/sortable to v9 (#33051) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e9f6364026..3b828ce3f8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-typescript": "^7.21.5", "@babel/runtime": "^7.22.3", "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/sortable": "^9.0.0", "@dnd-kit/utilities": "^3.2.2", "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", diff --git a/yarn.lock b/yarn.lock index 86fbc7e184..28372d6ae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2004,16 +2004,16 @@ __metadata: languageName: node linkType: hard -"@dnd-kit/sortable@npm:^8.0.0": - version: 8.0.0 - resolution: "@dnd-kit/sortable@npm:8.0.0" +"@dnd-kit/sortable@npm:^9.0.0": + version: 9.0.0 + resolution: "@dnd-kit/sortable@npm:9.0.0" dependencies: "@dnd-kit/utilities": "npm:^3.2.2" tslib: "npm:^2.0.0" peerDependencies: - "@dnd-kit/core": ^6.1.0 + "@dnd-kit/core": ^6.2.0 react: ">=16.8.0" - checksum: 10c0/a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb + checksum: 10c0/30566ec05371bd59729c0fb87537d78cd1760f08e4b49b5fa8298ebd3cb9f29fc258a48425c6a060b9efeca88e36a059000e770d630681986626abcc3589e97a languageName: node linkType: hard @@ -2843,7 +2843,7 @@ __metadata: "@babel/preset-typescript": "npm:^7.21.5" "@babel/runtime": "npm:^7.22.3" "@dnd-kit/core": "npm:^6.1.0" - "@dnd-kit/sortable": "npm:^8.0.0" + "@dnd-kit/sortable": "npm:^9.0.0" "@dnd-kit/utilities": "npm:^3.2.2" "@formatjs/cli": "npm:^6.1.1" "@formatjs/intl-pluralrules": "npm:^5.2.2" From b702cd74f328fa6fa3d25fbc6ff9291c79812a80 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:26:50 +0000 Subject: [PATCH 06/15] Update dependency @dnd-kit/core to v6.2.0 (#33050) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 28372d6ae3..a8fde8ab51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1979,28 +1979,28 @@ __metadata: languageName: node linkType: hard -"@dnd-kit/accessibility@npm:^3.1.0": - version: 3.1.0 - resolution: "@dnd-kit/accessibility@npm:3.1.0" +"@dnd-kit/accessibility@npm:^3.1.1": + version: 3.1.1 + resolution: "@dnd-kit/accessibility@npm:3.1.1" dependencies: tslib: "npm:^2.0.0" peerDependencies: react: ">=16.8.0" - checksum: 10c0/4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7 + checksum: 10c0/be0bf41716dc58f9386bc36906ec1ce72b7b42b6d1d0e631d347afe9bd8714a829bd6f58a346dd089b1519e93918ae2f94497411a61a4f5e4d9247c6cfd1fef8 languageName: node linkType: hard "@dnd-kit/core@npm:^6.1.0": - version: 6.1.0 - resolution: "@dnd-kit/core@npm:6.1.0" + version: 6.2.0 + resolution: "@dnd-kit/core@npm:6.2.0" dependencies: - "@dnd-kit/accessibility": "npm:^3.1.0" + "@dnd-kit/accessibility": "npm:^3.1.1" "@dnd-kit/utilities": "npm:^3.2.2" tslib: "npm:^2.0.0" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10c0/c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584 + checksum: 10c0/478d6bb027441b0e5fa5ecd9a4da8a5876811505147303de1a5a0783a4418c5f7f464bd3eb07b4be77ef7626364d1b905dc2a4f9055093b845cf39e1d6f13b73 languageName: node linkType: hard From 6efa320feb1ea2dac253f4875559881765891cde Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 26 Nov 2024 03:09:04 -0500 Subject: [PATCH 07/15] Fix `Style/SafeNavigation` cop (#32970) --- app/controllers/concerns/cache_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 1823b5b8ed..b1b09f2aab 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -28,7 +28,7 @@ module CacheConcern def render_with_cache(**options) raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? - key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') + key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes body = Rails.cache.read(key, raw: true) From 08914516d9915ddff499bb5d40b775bbf7a5adcc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:09:34 +0100 Subject: [PATCH 08/15] Update dependency postcss-preset-env to v10.1.1 (#32947) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index a8fde8ab51..a2c9dc80e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1837,7 +1837,7 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-random-function@npm:^1.0.0": +"@csstools/postcss-random-function@npm:^1.0.1": version: 1.0.1 resolution: "@csstools/postcss-random-function@npm:1.0.1" dependencies: @@ -1876,16 +1876,16 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-sign-functions@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-sign-functions@npm:1.0.0" +"@csstools/postcss-sign-functions@npm:^1.1.0": + version: 1.1.0 + resolution: "@csstools/postcss-sign-functions@npm:1.1.0" dependencies: "@csstools/css-calc": "npm:^2.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.4" "@csstools/css-tokenizer": "npm:^3.0.3" peerDependencies: postcss: ^8.4 - checksum: 10c0/ec745b2f1e714ffead43ade5964234dfc1750c3a71d2e29df862ab3f79ba4a1275187b270b4c226bbb1155bee8e9e63c35597b4f4cb3effaa632e5e07e422344 + checksum: 10c0/503bbaa8fe1d1a619880d5d6b838f07f1898a5820889e5db3c4e02bb8b340dab18b88f439f9f1da44c6669bab2d4ba3f9543643ccc459d8a21191c5d22109c9b languageName: node linkType: hard @@ -13850,8 +13850,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.0.0": - version: 10.1.0 - resolution: "postcss-preset-env@npm:10.1.0" + version: 10.1.1 + resolution: "postcss-preset-env@npm:10.1.1" dependencies: "@csstools/postcss-cascade-layers": "npm:^5.0.1" "@csstools/postcss-color-function": "npm:^4.0.6" @@ -13877,10 +13877,10 @@ __metadata: "@csstools/postcss-normalize-display-values": "npm:^4.0.0" "@csstools/postcss-oklab-function": "npm:^4.0.6" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" - "@csstools/postcss-random-function": "npm:^1.0.0" + "@csstools/postcss-random-function": "npm:^1.0.1" "@csstools/postcss-relative-color-syntax": "npm:^3.0.6" "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" - "@csstools/postcss-sign-functions": "npm:^1.0.0" + "@csstools/postcss-sign-functions": "npm:^1.1.0" "@csstools/postcss-stepped-value-functions": "npm:^4.0.5" "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.1" "@csstools/postcss-trigonometric-functions": "npm:^4.0.5" @@ -13918,7 +13918,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/bd157dbed38c3c125b3bf86f5437a8094539ec5bf24428487c7bbf29da393731e48053afc695494cc9dbe4d182cfe405c398fcf0b22eb326b6db395e7315f892 + checksum: 10c0/99931117735a66827c7318be023ddb614990457617ccbe7fd2fdc1f10345554652df180d4842768d68d57e14fc0be4d86d0b413c65e77e02db5511e57ed07c4f languageName: node linkType: hard From 7ba19ecf1e684781d4265cb076f10e1cf579eb87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:09:54 +0100 Subject: [PATCH 09/15] Update dependency webauthn to v3.2.2 (#32879) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cfda83d324..ab60bd9d0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,7 +93,6 @@ GEM annotaterb (4.13.0) ast (2.4.2) attr_required (1.0.2) - awrence (1.2.1) aws-eventstream (1.3.0) aws-partitions (1.1012.0) aws-sdk-core (3.213.0) @@ -349,7 +348,8 @@ GEM json-schema (5.1.0) addressable (~> 2.8) jsonapi-renderer (0.2.2) - jwt (2.7.1) + jwt (2.9.3) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -844,9 +844,8 @@ GEM public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (3.1.0) + webauthn (3.2.2) android_key_attestation (~> 0.3.0) - awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) From 5c3a64dd50f8279335ed4ad3df341c91e4f94024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:10:08 +0100 Subject: [PATCH 10/15] Update dependency aws-sdk-s3 to v1.174.0 (#33076) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ab60bd9d0a..3bbfb33d74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,8 +94,8 @@ GEM ast (2.4.2) attr_required (1.0.2) aws-eventstream (1.3.0) - aws-partitions (1.1012.0) - aws-sdk-core (3.213.0) + aws-partitions (1.1013.0) + aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -103,7 +103,7 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.173.0) + aws-sdk-s3 (1.174.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) From f0855fd41f0cc726c0d2dbdf19f311a1ee6729dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:10:19 +0100 Subject: [PATCH 11/15] Update dependency axios to v1.7.8 (#33075) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a2c9dc80e9..ae6e1aa9aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5156,13 +5156,13 @@ __metadata: linkType: hard "axios@npm:^1.4.0": - version: 1.7.7 - resolution: "axios@npm:1.7.7" + version: 1.7.8 + resolution: "axios@npm:1.7.8" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/4499efc89e86b0b49ffddc018798de05fab26e3bf57913818266be73279a6418c3ce8f9e934c7d2d707ab8c095e837fc6c90608fb7715b94d357720b5f568af7 + checksum: 10c0/23ae2d0105aea9170c34ac9b6f30d9b2ab2fa8b1370205d2f7ce98b9f9510ab420148c13359ee837ea5a4bf2fb028ff225bd2fc92052fb0c478c6b4a836e2d5f languageName: node linkType: hard From a27bafa59653328a0f06bedb1dfb2b6ee92af43d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 26 Nov 2024 04:45:47 -0500 Subject: [PATCH 12/15] Add `UserRole#bypass_block?` method for notification check (#32974) --- app/models/user_role.rb | 4 ++++ app/services/notify_service.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 23cc28b9b7..815a894088 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -142,6 +142,10 @@ class UserRole < ApplicationRecord other_role.nil? || position > other_role.position end + def bypass_block?(role) + overrides?(role) && highlighted? && can?(*Flags::CATEGORIES[:moderation]) + end + def computed_permissions # If called on the everyone role, no further computation needed return permissions if everyone? diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 0cf56c5a24..e87e5024fe 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -134,7 +134,7 @@ class NotifyService < BaseService end def from_staff? - @sender.local? && @sender.user.present? && @sender.user_role&.overrides?(@recipient.user_role) && @sender.user_role&.highlighted? && @sender.user_role&.can?(*UserRole::Flags::CATEGORIES[:moderation]) + @sender.local? && @sender.user.present? && @sender.user_role&.bypass_block?(@recipient.user_role) end def from_self? From 429e08e3d244b71e704fd54096c41b533b4ad2d5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 Nov 2024 10:59:11 +0100 Subject: [PATCH 13/15] Remove old notifications route from web UI (#33038) --- .../notifications/components/filter_bar.jsx | 119 ------- .../containers/filter_bar_container.js | 17 - .../mastodon/features/notifications/index.jsx | 308 ------------------ .../features/notifications_wrapper.jsx | 9 - .../features/ui/components/columns_area.jsx | 4 +- app/javascript/mastodon/features/ui/index.jsx | 4 +- .../features/ui/util/async-components.js | 10 +- 7 files changed, 5 insertions(+), 466 deletions(-) delete mode 100644 app/javascript/mastodon/features/notifications/components/filter_bar.jsx delete mode 100644 app/javascript/mastodon/features/notifications/containers/filter_bar_container.js delete mode 100644 app/javascript/mastodon/features/notifications/index.jsx delete mode 100644 app/javascript/mastodon/features/notifications_wrapper.jsx diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx deleted file mode 100644 index c288c2c0de..0000000000 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; -import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; -import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; -import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; -import StarIcon from '@/material-icons/400-24px/star.svg?react'; -import { Icon } from 'mastodon/components/icon'; - -const tooltips = defineMessages({ - mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, - favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' }, - boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, - polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, - follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, - statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' }, -}); - -class FilterBar extends PureComponent { - - static propTypes = { - selectFilter: PropTypes.func.isRequired, - selectedFilter: PropTypes.string.isRequired, - advancedMode: PropTypes.bool.isRequired, - intl: PropTypes.object.isRequired, - }; - - onClick (notificationType) { - return () => this.props.selectFilter(notificationType); - } - - render () { - const { selectedFilter, advancedMode, intl } = this.props; - const renderedElement = !advancedMode ? ( -
- - -
- ) : ( -
- - - - - - - -
- ); - return renderedElement; - } - -} - -export default injectIntl(FilterBar); diff --git a/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js b/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js deleted file mode 100644 index 4e0184cef3..0000000000 --- a/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; - -import { setFilter } from '../../../actions/notifications'; -import FilterBar from '../components/filter_bar'; - -const makeMapStateToProps = state => ({ - selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']), - advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']), -}); - -const mapDispatchToProps = (dispatch) => ({ - selectFilter (newActiveFilter) { - dispatch(setFilter(newActiveFilter)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar); diff --git a/app/javascript/mastodon/features/notifications/index.jsx b/app/javascript/mastodon/features/notifications/index.jsx deleted file mode 100644 index cefbd544b0..0000000000 --- a/app/javascript/mastodon/features/notifications/index.jsx +++ /dev/null @@ -1,308 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import { createSelector } from '@reduxjs/toolkit'; -import { List as ImmutableList } from 'immutable'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; - -import { debounce } from 'lodash'; - -import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; -import { compareId } from 'mastodon/compare_id'; -import { Icon } from 'mastodon/components/icon'; -import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; - -import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import { submitMarkers } from '../../actions/markers'; -import { - expandNotifications, - scrollTopNotifications, - loadPending, - mountNotifications, - unmountNotifications, - markNotificationsAsRead, -} from '../../actions/notifications'; -import Column from '../../components/column'; -import ColumnHeader from '../../components/column_header'; -import { LoadGap } from '../../components/load_gap'; -import ScrollableList from '../../components/scrollable_list'; - -import { - FilteredNotificationsBanner, - FilteredNotificationsIconButton, -} from './components/filtered_notifications_banner'; -import NotificationsPermissionBanner from './components/notifications_permission_banner'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import FilterBarContainer from './containers/filter_bar_container'; -import NotificationContainer from './containers/notification_container'; - -const messages = defineMessages({ - title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' }, -}); - -const getExcludedTypes = createSelector([ - state => state.getIn(['settings', 'notifications', 'shows']), -], (shows) => { - return ImmutableList(shows.filter(item => !item).keys()); -}); - -const getNotifications = createSelector([ - state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']), - state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']), - getExcludedTypes, - state => state.getIn(['notifications', 'items']), -], (showFilterBar, allowedType, excludedTypes, notifications) => { - if (!showFilterBar || allowedType === 'all') { - // used if user changed the notification settings after loading the notifications from the server - // otherwise a list of notifications will come pre-filtered from the backend - // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category - return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); - } - return notifications.filter(item => item === null || allowedType === item.get('type')); -}); - -const mapStateToProps = state => ({ - notifications: getNotifications(state), - isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0, - isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, - hasMore: state.getIn(['notifications', 'hasMore']), - numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, - lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', - canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), - needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']), -}); - -class Notifications extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - columnId: PropTypes.string, - notifications: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - hasMore: PropTypes.bool, - numPending: PropTypes.number, - lastReadId: PropTypes.string, - canMarkAsRead: PropTypes.bool, - needsNotificationPermission: PropTypes.bool, - }; - - static defaultProps = { - trackScroll: true, - }; - - UNSAFE_componentWillMount() { - this.props.dispatch(mountNotifications()); - } - - componentWillUnmount () { - this.handleLoadOlder.cancel(); - this.handleScrollToTop.cancel(); - this.handleScroll.cancel(); - this.props.dispatch(scrollTopNotifications(false)); - this.props.dispatch(unmountNotifications()); - } - - handleLoadGap = (maxId) => { - this.props.dispatch(expandNotifications({ maxId })); - }; - - handleLoadOlder = debounce(() => { - const last = this.props.notifications.last(); - this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); - }, 300, { leading: true }); - - handleLoadPending = () => { - this.props.dispatch(loadPending()); - }; - - handleScrollToTop = debounce(() => { - this.props.dispatch(scrollTopNotifications(true)); - }, 100); - - handleScroll = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - }, 100); - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('NOTIFICATIONS', {})); - } - }; - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - }; - - setColumnRef = c => { - this.column = c; - }; - - handleMoveUp = id => { - const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; - this._selectChild(elementIndex, true); - }; - - handleMoveDown = id => { - const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; - this._selectChild(elementIndex, false); - }; - - _selectChild (index, align_top) { - const container = this.column.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } - } - - handleMarkAsRead = () => { - this.props.dispatch(markNotificationsAsRead()); - this.props.dispatch(submitMarkers({ immediate: true })); - }; - - render () { - const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; - const pinned = !!columnId; - const emptyMessage = ; - const { signedIn } = this.props.identity; - - let scrollableContent = null; - - const filterBarContainer = signedIn - ? () - : null; - - if (isLoading && this.scrollableContent) { - scrollableContent = this.scrollableContent; - } else if (notifications.size > 0 || hasMore) { - scrollableContent = notifications.map((item, index) => item === null ? ( - 0 ? notifications.getIn([index - 1, 'id']) : null} - onClick={this.handleLoadGap} - /> - ) : ( - 0} - /> - )); - } else { - scrollableContent = null; - } - - this.scrollableContent = scrollableContent; - - let scrollContainer; - - const prepend = ( - <> - {needsNotificationPermission && } - - - ); - - if (signedIn) { - scrollContainer = ( - - {scrollableContent} - - ); - } else { - scrollContainer = ; - } - - const extraButton = ( - <> - - {canMarkAsRead && ( - - )} - - ); - - return ( - - - - - - {filterBarContainer} - - {scrollContainer} - - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default connect(mapStateToProps)(withIdentity(injectIntl(Notifications))); diff --git a/app/javascript/mastodon/features/notifications_wrapper.jsx b/app/javascript/mastodon/features/notifications_wrapper.jsx deleted file mode 100644 index 50383d5ebf..0000000000 --- a/app/javascript/mastodon/features/notifications_wrapper.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import Notifications_v2 from 'mastodon/features/notifications_v2'; - -export const NotificationsWrapper = (props) => { - return ( - - ); -}; - -export default NotificationsWrapper; \ No newline at end of file diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index de957a79b6..97b54e50d3 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -8,7 +8,7 @@ import { scrollRight } from '../../../scroll'; import BundleContainer from '../containers/bundle_container'; import { Compose, - NotificationsWrapper, + Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, @@ -30,7 +30,7 @@ import NavigationPanel from './navigation_panel'; const componentMap = { 'COMPOSE': Compose, 'HOME': HomeTimeline, - 'NOTIFICATIONS': NotificationsWrapper, + 'NOTIFICATIONS': Notifications, 'PUBLIC': PublicTimeline, 'REMOTE': PublicTimeline, 'COMMUNITY': CommunityTimeline, diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index daa4585ead..4e5cd4bd64 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -49,7 +49,7 @@ import { Favourites, DirectTimeline, HashtagTimeline, - NotificationsWrapper, + Notifications, NotificationRequests, NotificationRequest, FollowRequests, @@ -211,7 +211,7 @@ class SwitchingColumnsArea extends PureComponent { - + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 5a85c856d2..26bb7cd1e0 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -7,15 +7,7 @@ export function Compose () { } export function Notifications () { - return import(/* webpackChunkName: "features/notifications_v1" */'../../notifications'); -} - -export function Notifications_v2 () { - return import(/* webpackChunkName: "features/notifications_v2" */'../../notifications_v2'); -} - -export function NotificationsWrapper () { - return import(/* webpackChunkName: "features/notifications" */'../../notifications_wrapper'); + return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2'); } export function HomeTimeline () { From 7a3dea385e48c72ff4d1553709f618bc5070b255 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 Nov 2024 17:10:12 +0100 Subject: [PATCH 14/15] Change onboarding flow in web UI (#32998) --- .../mastodon/actions/suggestions.js | 58 --- .../mastodon/actions/suggestions.ts | 24 ++ app/javascript/mastodon/api/suggestions.ts | 8 + .../mastodon/api_types/suggestions.ts | 13 + .../mastodon/components/account.jsx | 20 +- .../components/column_back_button.tsx | 2 +- .../components/column_search_header.tsx | 67 ++++ .../mastodon/components/follow_button.tsx | 7 +- .../mastodon/features/explore/suggestions.jsx | 19 +- .../components/inline_follow_suggestions.jsx | 217 ------------ .../components/inline_follow_suggestions.tsx | 326 +++++++++++++++++ .../mastodon/features/lists/members.tsx | 123 ++----- .../features/onboarding/components/step.jsx | 57 --- .../mastodon/features/onboarding/follows.jsx | 62 ---- .../mastodon/features/onboarding/follows.tsx | 191 ++++++++++ .../mastodon/features/onboarding/index.jsx | 91 ----- .../mastodon/features/onboarding/profile.jsx | 162 --------- .../mastodon/features/onboarding/profile.tsx | 329 ++++++++++++++++++ .../mastodon/features/onboarding/share.jsx | 120 ------- app/javascript/mastodon/features/ui/index.jsx | 6 +- .../features/ui/util/async-components.js | 8 +- app/javascript/mastodon/locales/en.json | 32 +- app/javascript/mastodon/models/suggestion.ts | 12 + app/javascript/mastodon/reducers/index.ts | 4 +- .../mastodon/reducers/suggestions.js | 40 --- .../mastodon/reducers/suggestions.ts | 60 ++++ .../styles/mastodon-light/diff.scss | 19 +- .../styles/mastodon-light/variables.scss | 1 + app/javascript/styles/mastodon/_mixins.scss | 2 +- .../styles/mastodon/components.scss | 237 ++----------- app/javascript/styles/mastodon/forms.scss | 6 +- app/javascript/styles/mastodon/variables.scss | 2 + 32 files changed, 1142 insertions(+), 1183 deletions(-) delete mode 100644 app/javascript/mastodon/actions/suggestions.js create mode 100644 app/javascript/mastodon/actions/suggestions.ts create mode 100644 app/javascript/mastodon/api/suggestions.ts create mode 100644 app/javascript/mastodon/api_types/suggestions.ts create mode 100644 app/javascript/mastodon/components/column_search_header.tsx delete mode 100644 app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx create mode 100644 app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx delete mode 100644 app/javascript/mastodon/features/onboarding/components/step.jsx delete mode 100644 app/javascript/mastodon/features/onboarding/follows.jsx create mode 100644 app/javascript/mastodon/features/onboarding/follows.tsx delete mode 100644 app/javascript/mastodon/features/onboarding/index.jsx delete mode 100644 app/javascript/mastodon/features/onboarding/profile.jsx create mode 100644 app/javascript/mastodon/features/onboarding/profile.tsx delete mode 100644 app/javascript/mastodon/features/onboarding/share.jsx create mode 100644 app/javascript/mastodon/models/suggestion.ts delete mode 100644 app/javascript/mastodon/reducers/suggestions.js create mode 100644 app/javascript/mastodon/reducers/suggestions.ts diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js deleted file mode 100644 index 258ffa901d..0000000000 --- a/app/javascript/mastodon/actions/suggestions.js +++ /dev/null @@ -1,58 +0,0 @@ -import api from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; -export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; -export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; - -export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; - -export function fetchSuggestions(withRelationships = false) { - return (dispatch) => { - dispatch(fetchSuggestionsRequest()); - - api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => { - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(fetchSuggestionsSuccess(response.data)); - - if (withRelationships) { - dispatch(fetchRelationships(response.data.map(item => item.account.id))); - } - }).catch(error => dispatch(fetchSuggestionsFail(error))); - }; -} - -export function fetchSuggestionsRequest() { - return { - type: SUGGESTIONS_FETCH_REQUEST, - skipLoading: true, - }; -} - -export function fetchSuggestionsSuccess(suggestions) { - return { - type: SUGGESTIONS_FETCH_SUCCESS, - suggestions, - skipLoading: true, - }; -} - -export function fetchSuggestionsFail(error) { - return { - type: SUGGESTIONS_FETCH_FAIL, - error, - skipLoading: true, - skipAlert: true, - }; -} - -export const dismissSuggestion = accountId => (dispatch) => { - dispatch({ - type: SUGGESTIONS_DISMISS, - id: accountId, - }); - - api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {}); -}; diff --git a/app/javascript/mastodon/actions/suggestions.ts b/app/javascript/mastodon/actions/suggestions.ts new file mode 100644 index 0000000000..0eadfa6b47 --- /dev/null +++ b/app/javascript/mastodon/actions/suggestions.ts @@ -0,0 +1,24 @@ +import { + apiGetSuggestions, + apiDeleteSuggestion, +} from 'mastodon/api/suggestions'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const fetchSuggestions = createDataLoadingThunk( + 'suggestions/fetch', + () => apiGetSuggestions(20), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data.map((x) => x.account))); + dispatch(fetchRelationships(data.map((x) => x.account.id))); + + return data; + }, +); + +export const dismissSuggestion = createDataLoadingThunk( + 'suggestions/dismiss', + ({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId), +); diff --git a/app/javascript/mastodon/api/suggestions.ts b/app/javascript/mastodon/api/suggestions.ts new file mode 100644 index 0000000000..d4817698cc --- /dev/null +++ b/app/javascript/mastodon/api/suggestions.ts @@ -0,0 +1,8 @@ +import { apiRequestGet, apiRequestDelete } from 'mastodon/api'; +import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions'; + +export const apiGetSuggestions = (limit: number) => + apiRequestGet('v2/suggestions', { limit }); + +export const apiDeleteSuggestion = (accountId: string) => + apiRequestDelete(`v1/suggestions/${accountId}`); diff --git a/app/javascript/mastodon/api_types/suggestions.ts b/app/javascript/mastodon/api_types/suggestions.ts new file mode 100644 index 0000000000..7d91daf901 --- /dev/null +++ b/app/javascript/mastodon/api_types/suggestions.ts @@ -0,0 +1,13 @@ +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; + +export type ApiSuggestionSourceJSON = + | 'featured' + | 'most_followed' + | 'most_interactions' + | 'similar_to_recently_followed' + | 'friends_of_friends'; + +export interface ApiSuggestionJSON { + sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]]; + account: ApiAccountJSON; +} diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index 265c68697b..fa66fd56bb 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -10,6 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { EmptyAccount } from 'mastodon/components/empty_account'; +import { FollowButton } from 'mastodon/components/follow_button'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; @@ -23,9 +24,6 @@ import { DisplayName } from './display_name'; import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, @@ -35,13 +33,9 @@ const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, }); -const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => { +const Account = ({ size = 46, account, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => { const intl = useIntl(); - const handleFollow = useCallback(() => { - onFollow(account); - }, [onFollow, account]); - const handleBlock = useCallback(() => { onBlock(account); }, [onBlock, account]); @@ -74,13 +68,12 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica let buttons; if (account.get('id') !== me && account.get('relationship', null) !== null) { - const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = + )} + + ); +}; diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 46314af309..faf9d8bdb8 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -99,7 +99,12 @@ export const FollowButton: React.FC<{ return ( - - - - -
-
- {suggestions.map(suggestion => ( - - ))} -
- - {canScrollLeft && ( - - )} - - {canScrollRight && ( - - )} -
- - ); -}; - -InlineFollowSuggestions.propTypes = { - hidden: PropTypes.bool, -}; diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx new file mode 100644 index 0000000000..5fd47443d9 --- /dev/null +++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx @@ -0,0 +1,326 @@ +import { useEffect, useCallback, useRef, useState } from 'react'; + +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; +import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import { changeSetting } from 'mastodon/actions/settings'; +import { + fetchSuggestions, + dismissSuggestion, +} from 'mastodon/actions/suggestions'; +import type { ApiSuggestionSourceJSON } from 'mastodon/api_types/suggestions'; +import { Avatar } from 'mastodon/components/avatar'; +import { DisplayName } from 'mastodon/components/display_name'; +import { FollowButton } from 'mastodon/components/follow_button'; +import { Icon } from 'mastodon/components/icon'; +import { IconButton } from 'mastodon/components/icon_button'; +import { VerifiedBadge } from 'mastodon/components/verified_badge'; +import { domain } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, + next: { id: 'lightbox.next', defaultMessage: 'Next' }, + dismiss: { + id: 'follow_suggestions.dismiss', + defaultMessage: "Don't show again", + }, + friendsOfFriendsHint: { + id: 'follow_suggestions.hints.friends_of_friends', + defaultMessage: 'This profile is popular among the people you follow.', + }, + similarToRecentlyFollowedHint: { + id: 'follow_suggestions.hints.similar_to_recently_followed', + defaultMessage: + 'This profile is similar to the profiles you have most recently followed.', + }, + featuredHint: { + id: 'follow_suggestions.hints.featured', + defaultMessage: 'This profile has been hand-picked by the {domain} team.', + }, + mostFollowedHint: { + id: 'follow_suggestions.hints.most_followed', + defaultMessage: 'This profile is one of the most followed on {domain}.', + }, + mostInteractionsHint: { + id: 'follow_suggestions.hints.most_interactions', + defaultMessage: + 'This profile has been recently getting a lot of attention on {domain}.', + }, +}); + +const Source: React.FC<{ + id: ApiSuggestionSourceJSON; +}> = ({ id }) => { + const intl = useIntl(); + + let label, hint; + + switch (id) { + case 'friends_of_friends': + hint = intl.formatMessage(messages.friendsOfFriendsHint); + label = ( + + ); + break; + case 'similar_to_recently_followed': + hint = intl.formatMessage(messages.similarToRecentlyFollowedHint); + label = ( + + ); + break; + case 'featured': + hint = intl.formatMessage(messages.featuredHint, { domain }); + label = ( + + ); + break; + case 'most_followed': + hint = intl.formatMessage(messages.mostFollowedHint, { domain }); + label = ( + + ); + break; + case 'most_interactions': + hint = intl.formatMessage(messages.mostInteractionsHint, { domain }); + label = ( + + ); + break; + } + + return ( +
+ + {label} +
+ ); +}; + +const Card: React.FC<{ + id: string; + sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]]; +}> = ({ id, sources }) => { + const intl = useIntl(); + const account = useAppSelector((state) => state.accounts.get(id)); + const firstVerifiedField = account?.fields.find((item) => !!item.verified_at); + const dispatch = useAppDispatch(); + + const handleDismiss = useCallback(() => { + void dispatch(dismissSuggestion({ accountId: id })); + }, [id, dispatch]); + + return ( +
+ + +
+ + + +
+ +
+ + + + {firstVerifiedField ? ( + + ) : ( + + )} +
+ + +
+ ); +}; + +const DISMISSIBLE_ID = 'home/follow-suggestions'; + +export const InlineFollowSuggestions: React.FC<{ + hidden?: boolean; +}> = ({ hidden }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const suggestions = useAppSelector((state) => state.suggestions.items); + const isLoading = useAppSelector((state) => state.suggestions.isLoading); + const dismissed = useAppSelector( + (state) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]) as boolean, + ); + const bodyRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + + useEffect(() => { + void dispatch(fetchSuggestions()); + }, [dispatch]); + + useEffect(() => { + if (!bodyRef.current) { + return; + } + + if (getComputedStyle(bodyRef.current).direction === 'rtl') { + setCanScrollLeft( + bodyRef.current.clientWidth - bodyRef.current.scrollLeft < + bodyRef.current.scrollWidth, + ); + setCanScrollRight(bodyRef.current.scrollLeft < 0); + } else { + setCanScrollLeft(bodyRef.current.scrollLeft > 0); + setCanScrollRight( + bodyRef.current.scrollLeft + bodyRef.current.clientWidth < + bodyRef.current.scrollWidth, + ); + } + }, [setCanScrollRight, setCanScrollLeft, suggestions]); + + const handleLeftNav = useCallback(() => { + if (!bodyRef.current) { + return; + } + + bodyRef.current.scrollLeft -= 200; + }, []); + + const handleRightNav = useCallback(() => { + if (!bodyRef.current) { + return; + } + + bodyRef.current.scrollLeft += 200; + }, []); + + const handleScroll = useCallback(() => { + if (!bodyRef.current) { + return; + } + + if (getComputedStyle(bodyRef.current).direction === 'rtl') { + setCanScrollLeft( + bodyRef.current.clientWidth - bodyRef.current.scrollLeft < + bodyRef.current.scrollWidth, + ); + setCanScrollRight(bodyRef.current.scrollLeft < 0); + } else { + setCanScrollLeft(bodyRef.current.scrollLeft > 0); + setCanScrollRight( + bodyRef.current.scrollLeft + bodyRef.current.clientWidth < + bodyRef.current.scrollWidth, + ); + } + }, [setCanScrollRight, setCanScrollLeft]); + + const handleDismiss = useCallback(() => { + dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true)); + }, [dispatch]); + + if (dismissed || (!isLoading && suggestions.length === 0)) { + return null; + } + + if (hidden) { + return
; + } + + return ( +
+
+

+ +

+ +
+ + + + +
+
+ +
+
+ {suggestions.map((suggestion) => ( + + ))} +
+ + {canScrollLeft && ( + + )} + + {canScrollRight && ( + + )} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index a1b50ffaf8..184b54b92d 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -7,11 +7,8 @@ import { useParams, Link } from 'react-router-dom'; import { useDebouncedCallback } from 'use-debounce'; -import AddIcon from '@/material-icons/400-24px/add.svg?react'; -import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react'; -import { fetchFollowing } from 'mastodon/actions/accounts'; import { importFetchedAccounts } from 'mastodon/actions/importer'; import { fetchList } from 'mastodon/actions/lists'; import { apiRequest } from 'mastodon/api'; @@ -25,14 +22,12 @@ import { Avatar } from 'mastodon/components/avatar'; import { Button } from 'mastodon/components/button'; import Column from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; +import { ColumnSearchHeader } from 'mastodon/components/column_search_header'; import { FollowersCounter } from 'mastodon/components/counters'; import { DisplayName } from 'mastodon/components/display_name'; -import { Icon } from 'mastodon/components/icon'; import ScrollableList from 'mastodon/components/scrollable_list'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; -import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; -import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; const messages = defineMessages({ @@ -49,54 +44,6 @@ const messages = defineMessages({ type Mode = 'remove' | 'add'; -const ColumnSearchHeader: React.FC<{ - onBack: () => void; - onSubmit: (value: string) => void; -}> = ({ onBack, onSubmit }) => { - const intl = useIntl(); - const [value, setValue] = useState(''); - - const handleChange = useCallback( - ({ target: { value } }: React.ChangeEvent) => { - setValue(value); - onSubmit(value); - }, - [setValue, onSubmit], - ); - - const handleSubmit = useCallback(() => { - onSubmit(value); - }, [onSubmit, value]); - - return ( - -
- - - -
-
- ); -}; - const AccountItem: React.FC<{ accountId: string; listId: string; @@ -156,6 +103,7 @@ const AccountItem: React.FC<{ text={intl.formatMessage( partOfList ? messages.remove : messages.add, )} + secondary={partOfList} onClick={handleClick} />
@@ -171,9 +119,6 @@ const ListMembers: React.FC<{ const { id } = useParams<{ id: string }>(); const intl = useIntl(); - const followingAccountIds = useAppSelector( - (state) => state.user_lists.getIn(['following', me, 'items']) as string[], - ); const [searching, setSearching] = useState(false); const [accountIds, setAccountIds] = useState([]); const [searchAccountIds, setSearchAccountIds] = useState([]); @@ -195,8 +140,6 @@ const ListMembers: React.FC<{ .catch(() => { setLoading(false); }); - - dispatch(fetchFollowing(me)); } }, [dispatch, id]); @@ -265,8 +208,8 @@ const ListMembers: React.FC<{ let displayedAccountIds: string[]; - if (mode === 'add') { - displayedAccountIds = searching ? searchAccountIds : followingAccountIds; + if (mode === 'add' && searching) { + displayedAccountIds = searchAccountIds; } else { displayedAccountIds = accountIds; } @@ -276,31 +219,21 @@ const ListMembers: React.FC<{ bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)} > - {mode === 'remove' ? ( - - - - } - /> - ) : ( - - )} + + + - {displayedAccountIds.length > 0 &&
} + <> + {displayedAccountIds.length > 0 &&
} -
- - - -
- - ) +
+ + + +
+ } emptyMessage={ mode === 'remove' ? ( diff --git a/app/javascript/mastodon/features/onboarding/components/step.jsx b/app/javascript/mastodon/features/onboarding/components/step.jsx deleted file mode 100644 index a2a1653b8a..0000000000 --- a/app/javascript/mastodon/features/onboarding/components/step.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; - -import { Link } from 'react-router-dom'; - -import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react'; -import CheckIcon from '@/material-icons/400-24px/done.svg?react'; -import { Icon } from 'mastodon/components/icon'; - -export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => { - const content = ( - <> -
- -
- -
-
{label}
-

{description}

-
- -
- {completed ? : } -
- - ); - - if (href) { - return ( - - {content} - - ); - } else if (to) { - return ( - - {content} - - ); - } - - return ( - - ); -}; - -Step.propTypes = { - label: PropTypes.node, - description: PropTypes.node, - icon: PropTypes.string, - iconComponent: PropTypes.func, - completed: PropTypes.bool, - href: PropTypes.string, - to: PropTypes.string, - onClick: PropTypes.func, -}; diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx deleted file mode 100644 index e23a335c06..0000000000 --- a/app/javascript/mastodon/features/onboarding/follows.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useDispatch } from 'react-redux'; - - -import { fetchSuggestions } from 'mastodon/actions/suggestions'; -import { markAsPartial } from 'mastodon/actions/timelines'; -import { ColumnBackButton } from 'mastodon/components/column_back_button'; -import { EmptyAccount } from 'mastodon/components/empty_account'; -import Account from 'mastodon/containers/account_container'; -import { useAppSelector } from 'mastodon/store'; - -export const Follows = () => { - const dispatch = useDispatch(); - const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading'])); - const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items'])); - - useEffect(() => { - dispatch(fetchSuggestions(true)); - - return () => { - dispatch(markAsPartial('home')); - }; - }, [dispatch]); - - let loadedContent; - - if (isLoading) { - loadedContent = (new Array(8)).fill().map((_, i) => ); - } else if (suggestions.isEmpty()) { - loadedContent =
; - } else { - loadedContent = suggestions.map(suggestion => ); - } - - return ( - <> - - -
-
-

-

-
- -
- {loadedContent} -
- -

{chunks} }} />

- -
- -
-
- - ); -}; diff --git a/app/javascript/mastodon/features/onboarding/follows.tsx b/app/javascript/mastodon/features/onboarding/follows.tsx new file mode 100644 index 0000000000..25ee48c8ac --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/follows.tsx @@ -0,0 +1,191 @@ +import { useEffect, useState, useCallback, useRef } from 'react'; + +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; + +import { Helmet } from 'react-helmet'; +import { Link } from 'react-router-dom'; + +import { useDebouncedCallback } from 'use-debounce'; + +import PersonIcon from '@/material-icons/400-24px/person.svg?react'; +import { fetchRelationships } from 'mastodon/actions/accounts'; +import { importFetchedAccounts } from 'mastodon/actions/importer'; +import { fetchSuggestions } from 'mastodon/actions/suggestions'; +import { markAsPartial } from 'mastodon/actions/timelines'; +import { apiRequest } from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import Column from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import { ColumnSearchHeader } from 'mastodon/components/column_search_header'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import Account from 'mastodon/containers/account_container'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +const messages = defineMessages({ + title: { + id: 'onboarding.follows.title', + defaultMessage: 'Follow people to get started', + }, + search: { id: 'onboarding.follows.search', defaultMessage: 'Search' }, + back: { id: 'onboarding.follows.back', defaultMessage: 'Back' }, +}); + +type Mode = 'remove' | 'add'; + +export const Follows: React.FC<{ + multiColumn?: boolean; +}> = ({ multiColumn }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const isLoading = useAppSelector((state) => state.suggestions.isLoading); + const suggestions = useAppSelector((state) => state.suggestions.items); + const [searchAccountIds, setSearchAccountIds] = useState([]); + const [mode, setMode] = useState('remove'); + const [isLoadingSearch, setIsLoadingSearch] = useState(false); + const [isSearching, setIsSearching] = useState(false); + + useEffect(() => { + void dispatch(fetchSuggestions()); + + return () => { + dispatch(markAsPartial('home')); + }; + }, [dispatch]); + + const handleSearchClick = useCallback(() => { + setMode('add'); + }, [setMode]); + + const handleDismissSearchClick = useCallback(() => { + setMode('remove'); + setIsSearching(false); + }, [setMode, setIsSearching]); + + const searchRequestRef = useRef(null); + + const handleSearch = useDebouncedCallback( + (value: string) => { + if (searchRequestRef.current) { + searchRequestRef.current.abort(); + } + + if (value.trim().length === 0) { + setIsSearching(false); + setSearchAccountIds([]); + return; + } + + setIsSearching(true); + setIsLoadingSearch(true); + + searchRequestRef.current = new AbortController(); + + void apiRequest('GET', 'v1/accounts/search', { + signal: searchRequestRef.current.signal, + params: { + q: value, + }, + }) + .then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((a) => a.id))); + setSearchAccountIds(data.map((a) => a.id)); + setIsLoadingSearch(false); + return ''; + }) + .catch(() => { + setIsLoadingSearch(false); + }); + }, + 500, + { leading: true, trailing: true }, + ); + + let displayedAccountIds: string[]; + + if (mode === 'add' && isSearching) { + displayedAccountIds = searchAccountIds; + } else { + displayedAccountIds = suggestions.map( + (suggestion) => suggestion.account_id, + ); + } + + return ( + + + + + + + {displayedAccountIds.length > 0 &&
} + +
+ + + +
+ + } + emptyMessage={ + mode === 'remove' ? ( + + ) : ( + + ) + } + > + {displayedAccountIds.map((accountId) => ( + + ))} + + + + {intl.formatMessage(messages.title)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default Follows; diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx deleted file mode 100644 index d100a1c3d5..0000000000 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useCallback } from 'react'; - -import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; - -import { Helmet } from 'react-helmet'; -import { Link, Switch, Route } from 'react-router-dom'; - -import { useDispatch } from 'react-redux'; - - -import illustration from '@/images/elephant_ui_conversation.svg'; -import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react'; -import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react'; -import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; -import EditNoteIcon from '@/material-icons/400-24px/edit_note.svg?react'; -import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; -import { focusCompose } from 'mastodon/actions/compose'; -import { Icon } from 'mastodon/components/icon'; -import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; -import Column from 'mastodon/features/ui/components/column'; -import { me } from 'mastodon/initial_state'; -import { useAppSelector } from 'mastodon/store'; -import { assetHost } from 'mastodon/utils/config'; - -import { Step } from './components/step'; -import { Follows } from './follows'; -import { Profile } from './profile'; -import { Share } from './share'; - -const messages = defineMessages({ - template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' }, -}); - -const Onboarding = () => { - const account = useAppSelector(state => state.getIn(['accounts', me])); - const dispatch = useDispatch(); - const intl = useIntl(); - - const handleComposeClick = useCallback(() => { - dispatch(focusCompose(intl.formatMessage(messages.template))); - }, [dispatch, intl]); - - return ( - - {account ? ( - - -
-
- -

-

-
- -
- 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={} description={} /> - = 1} icon='user-plus' iconComponent={PersonAddIcon} label={} description={} /> - = 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={} description={ }} />} /> - } description={} /> -
- -

- -
- - - - - - - - - -
-
-
- - - - -
- ) : } - - - - -
- ); -}; - -export default Onboarding; diff --git a/app/javascript/mastodon/features/onboarding/profile.jsx b/app/javascript/mastodon/features/onboarding/profile.jsx deleted file mode 100644 index 14250ae39b..0000000000 --- a/app/javascript/mastodon/features/onboarding/profile.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useState, useMemo, useCallback, createRef } from 'react'; - -import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { useHistory } from 'react-router-dom'; - - -import { useDispatch } from 'react-redux'; - -import Toggle from 'react-toggle'; - -import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react'; -import EditIcon from '@/material-icons/400-24px/edit.svg?react'; -import { updateAccount } from 'mastodon/actions/accounts'; -import { Button } from 'mastodon/components/button'; -import { ColumnBackButton } from 'mastodon/components/column_back_button'; -import { Icon } from 'mastodon/components/icon'; -import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import { me } from 'mastodon/initial_state'; -import { useAppSelector } from 'mastodon/store'; -import { unescapeHTML } from 'mastodon/utils/html'; - -const messages = defineMessages({ - uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' }, - uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' }, -}); - -const nullIfMissing = path => path.endsWith('missing.png') ? null : path; - -export const Profile = () => { - const account = useAppSelector(state => state.getIn(['accounts', me])); - const [displayName, setDisplayName] = useState(account.get('display_name')); - const [note, setNote] = useState(unescapeHTML(account.get('note'))); - const [avatar, setAvatar] = useState(null); - const [header, setHeader] = useState(null); - const [discoverable, setDiscoverable] = useState(account.get('discoverable')); - const [isSaving, setIsSaving] = useState(false); - const [errors, setErrors] = useState(); - const avatarFileRef = createRef(); - const headerFileRef = createRef(); - const dispatch = useDispatch(); - const intl = useIntl(); - const history = useHistory(); - - const handleDisplayNameChange = useCallback(e => { - setDisplayName(e.target.value); - }, [setDisplayName]); - - const handleNoteChange = useCallback(e => { - setNote(e.target.value); - }, [setNote]); - - const handleDiscoverableChange = useCallback(e => { - setDiscoverable(e.target.checked); - }, [setDiscoverable]); - - const handleAvatarChange = useCallback(e => { - setAvatar(e.target?.files?.[0]); - }, [setAvatar]); - - const handleHeaderChange = useCallback(e => { - setHeader(e.target?.files?.[0]); - }, [setHeader]); - - const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : nullIfMissing(account.get('avatar')), [avatar, account]); - const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : nullIfMissing(account.get('header')), [header, account]); - - const handleSubmit = useCallback(() => { - setIsSaving(true); - - dispatch(updateAccount({ - displayName, - note, - avatar, - header, - discoverable, - indexable: discoverable, - })).then(() => history.push('/start/follows')).catch(err => { - setIsSaving(false); - setErrors(err.response.data.details); - }); - }, [dispatch, displayName, note, avatar, header, discoverable, history]); - - return ( - <> - - -
-
-

-

-
- -
-
- - - -
- -
- - -
- -
-
- -
- - -
-