- {!banner &&
}
{banner &&
diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json
index 6c37cdf5ca..d1873d6dce 100644
--- a/app/javascript/mastodon/locales/af.json
+++ b/app/javascript/mastodon/locales/af.json
@@ -3,6 +3,7 @@
"about.contact": "Kontak:",
"about.disclaimer": "Mastodon is gratis oopbronsagteware en ’n handelsmerk van Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie",
+ "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
"about.domain_blocks.silenced.title": "Beperk",
"about.domain_blocks.suspended.title": "Opgeskort",
"about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 7d1049a30f..c763a32ba8 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -521,7 +521,7 @@
"poll.total_people": "{count, plural, one {# persona} other {# persones}}",
"poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
"poll.vote": "Vota",
- "poll.voted": "Vas votar per aquesta resposta",
+ "poll.voted": "Vau votar aquesta resposta",
"poll.votes": "{votes, plural, one {# vot} other {# vots}}",
"poll_button.add_poll": "Afegeix una enquesta",
"poll_button.remove_poll": "Elimina l'enquesta",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 9567fda4bb..c44ec13fb5 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -96,7 +96,6 @@
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
- "autosuggest_hashtag.per_week": "{count} per week",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report",
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
@@ -161,23 +160,23 @@
"compose_form.markdown.unmarked": "Markdown is NOT available",
"compose_form.mention_warning": "When you add a mention to a limited post, the person you are mentioning can also see this post.",
"compose_form.placeholder": "What's on your mind?",
- "compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
- "compose_form.poll.add_option": "Add a choice",
+ "compose_form.poll.add_option": "Add option",
"compose_form.poll.duration": "Poll duration",
- "compose_form.poll.option_placeholder": "Choice {number}",
- "compose_form.poll.remove_option": "Remove this choice",
+ "compose_form.poll.multiple": "Multiple choice",
+ "compose_form.poll.option_placeholder": "Option {number}",
+ "compose_form.poll.remove_option": "Remove this option",
+ "compose_form.poll.single": "Pick one",
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
- "compose_form.publish": "Publish",
+ "compose_form.poll.type": "Style",
+ "compose_form.publish": "Post",
"compose_form.publish_form": "New post",
- "compose_form.publish_loud": "{publish}!",
- "compose_form.save_changes": "Save changes",
- "compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
- "compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
- "compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
+ "compose_form.reply": "Reply",
+ "compose_form.save_changes": "Update",
+ "compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
"compose_form.spoiler.marked": "Remove content warning",
"compose_form.spoiler.unmarked": "Add content warning",
- "compose_form.spoiler_placeholder": "Write your warning here",
+ "compose_form.spoiler_placeholder": "Content warning (optional)",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block",
@@ -428,7 +427,6 @@
"navigation_bar.direct": "Private mentions",
"navigation_bar.discover": "Discover",
"navigation_bar.domain_blocks": "Blocked domains",
- "navigation_bar.edit_profile": "Edit profile",
"navigation_bar.explore": "Explore",
"navigation_bar.favourites": "Favorites",
"navigation_bar.filters": "Muted words",
@@ -551,22 +549,23 @@
"poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll",
"privacy.change": "Change post privacy",
- "privacy.direct.long": "Visible for mentioned users only",
- "privacy.direct.short": "Mentioned people only",
+ "privacy.direct.long": "Everyone mentioned in the post",
+ "privacy.direct.short": "Specific people",
"privacy.limited.short": "Limited",
"privacy.login.long": "Visible for login users only",
"privacy.login.short": "Login only",
"privacy.mutual.long": "Mutual followers only",
"privacy.mutual.short": "Mutual only",
"privacy.personal.short": "Yourself only",
- "privacy.private.long": "Visible for followers only",
- "privacy.private.short": "Followers only",
- "privacy.public.long": "Visible for all",
+ "privacy.private.long": "Only your followers",
+ "privacy.private.short": "Followers",
+ "privacy.public.long": "Anyone on and off Mastodon",
"privacy.public.short": "Public",
"privacy.public_unlisted.long": "Visible for all without GTL",
"privacy.public_unlisted.short": "Public unlisted",
- "privacy.unlisted.long": "Visible for all, but opted-out of discovery features",
- "privacy.unlisted.short": "Unlisted",
+ "privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
+ "privacy.unlisted.long": "Fewer algorithmic fanfares",
+ "privacy.unlisted.short": "Quiet public",
"privacy_policy.last_updated": "Last updated {date}",
"privacy_policy.title": "Privacy Policy",
"reaction_deck.add": "Add",
@@ -586,7 +585,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "today",
+ "reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
"reply_indicator.cancel": "Cancel",
+ "reply_indicator.poll": "Poll",
"report.block": "Block",
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
"report.categories.legal": "Legal",
@@ -768,10 +769,8 @@
"upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
"upload_form.description": "Describe for people who are blind or have low vision",
- "upload_form.description_missing": "No description added",
"upload_form.edit": "Edit",
"upload_form.thumbnail": "Change thumbnail",
- "upload_form.undo": "Delete",
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
"upload_modal.analyzing_picture": "Analyzing picture…",
"upload_modal.apply": "Apply",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index ed5f98fbe9..8acf1a0e44 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -216,7 +216,7 @@
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
- "compose_form.limited_post_warning": "限定投稿は現状、ごく一部のMastodonサーバーにしか届きません(2023年9月時点でFedibird、kmyblueなど一部のみです)",
+ "compose_form.limited_post_warning": "限定投稿は、v4.3.0以降のMastodon、またはkmyblue・Fedibirdを含む一部のMastodonにしか届きません(2024年1月時点)",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.lock_disclaimer.lock": "承認制",
"compose_form.markdown.marked": "Markdown有効",
@@ -634,15 +634,15 @@
"poll_button.add_poll": "アンケートを追加",
"poll_button.remove_poll": "アンケートを削除",
"privacy.change": "公開範囲を変更",
- "privacy.circle.long": "サークルメンバーのみ",
- "privacy.circle.short": "サークル",
+ "privacy.circle.long": "サークルメンバーのみ閲覧可",
+ "privacy.circle.short": "サークル (投稿時点)",
"privacy.direct.long": "指定された相手のみ閲覧可",
"privacy.direct.short": "指定された相手のみ",
"privacy.limited.short": "限定投稿",
"privacy.login.long": "ログインユーザーのみ閲覧可、公開",
"privacy.login.short": "ログインユーザーのみ",
- "privacy.mutual.long": "相互フォローさんのみ閲覧可、限定投稿",
- "privacy.mutual.short": "相互のみ",
+ "privacy.mutual.long": "相互フォローのみ閲覧可",
+ "privacy.mutual.short": "相互 (投稿時点)",
"privacy.personal.short": "自分限定",
"privacy.private.long": "フォロワーのみ閲覧可",
"privacy.private.short": "フォロワーのみ",
@@ -650,6 +650,8 @@
"privacy.public.short": "公開",
"privacy.public_unlisted.long": "誰でも閲覧可、ホーム+ローカルTL",
"privacy.public_unlisted.short": "ローカル公開",
+ "privacy.reply.long": "元投稿と同じメンバーが閲覧可",
+ "privacy.reply.short": "限定投稿への返信",
"privacy.unlisted.long": "誰でも閲覧可、ホームTL",
"privacy.unlisted.short": "非収載",
"privacy_policy.last_updated": "{date}に更新",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 833bfe6ace..1ecfbcaf06 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -18,6 +18,7 @@
"account.blocked": "Blocat",
"account.browse_more_on_origin_server": "Navigar sul perfil original",
"account.cancel_follow_request": "Retirar la demanda d’abonament",
+ "account.copy": "Copiar lo ligam del perfil",
"account.direct": "Mencionar @{name} en privat",
"account.disable_notifications": "Quitar de m’avisar quand @{name} publica quicòm",
"account.domain_blocked": "Domeni amagat",
@@ -28,6 +29,7 @@
"account.featured_tags.last_status_never": "Cap de publicacion",
"account.featured_tags.title": "Etiquetas en avant de {name}",
"account.follow": "Sègre",
+ "account.follow_back": "Sègre en retorn",
"account.followers": "Seguidors",
"account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.",
"account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}",
@@ -48,6 +50,7 @@
"account.mute_notifications_short": "Amudir las notificacions",
"account.mute_short": "Amudir",
"account.muted": "Mes en silenci",
+ "account.mutual": "Mutual",
"account.no_bio": "Cap de descripcion pas fornida.",
"account.open_original_page": "Dobrir la pagina d’origina",
"account.posts": "Tuts",
@@ -172,6 +175,7 @@
"conversation.mark_as_read": "Marcar coma legida",
"conversation.open": "Veire la conversacion",
"conversation.with": "Amb {names}",
+ "copy_icon_button.copied": "Copiat al quichapapièr",
"copypaste.copied": "Copiat",
"copypaste.copy_to_clipboard": "Copiar al quichapapièr",
"directory.federated": "Del fediverse conegut",
@@ -294,6 +298,8 @@
"keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "far davalar dins la lista",
"keyboard_shortcuts.enter": "dobrir los estatuts",
+ "keyboard_shortcuts.favourite": "Marcar coma favorit",
+ "keyboard_shortcuts.favourites": "Dobrir la lista dels favorits",
"keyboard_shortcuts.federated": "dobrir lo flux public global",
"keyboard_shortcuts.heading": "Acorchis clavièr",
"keyboard_shortcuts.home": "dobrir lo flux public local",
@@ -339,6 +345,7 @@
"lists.search": "Cercar demest lo mond que seguètz",
"lists.subheading": "Vòstras listas",
"load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
+ "loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat",
"mute_modal.duration": "Durada",
"mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
@@ -371,6 +378,7 @@
"not_signed_in_indicator.not_signed_in": "Devètz vos connectar per accedir a aquesta ressorsa.",
"notification.admin.report": "{name} senhalèt {target}",
"notification.admin.sign_up": "{name} se marquèt",
+ "notification.favourite": "{name} a mes vòstre estatut en favorit",
"notification.follow": "{name} vos sèc",
"notification.follow_request": "{name} a demandat a vos sègre",
"notification.mention": "{name} vos a mencionat",
@@ -423,6 +431,8 @@
"onboarding.compose.template": "Adiu #Mastodon !",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
+ "onboarding.profile.display_name": "Nom d’afichatge",
+ "onboarding.profile.note": "Biografia",
"onboarding.share.title": "Partejar vòstre perfil",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
@@ -504,6 +514,7 @@
"report_notification.categories.spam": "Messatge indesirable",
"report_notification.categories.violation": "Violacion de las règlas",
"report_notification.open": "Dobrir lo senhalament",
+ "search.no_recent_searches": "Cap de recèrcas recentas",
"search.placeholder": "Recercar",
"search.search_or_paste": "Recercar o picar una URL",
"search_popout.language_code": "Còdi ISO de lenga",
@@ -536,6 +547,7 @@
"status.copy": "Copiar lo ligam de l’estatut",
"status.delete": "Escafar",
"status.detailed_status": "Vista detalhada de la convèrsa",
+ "status.direct": "Mencionar @{name} en privat",
"status.direct_indicator": "Mencion privada",
"status.edit": "Modificar",
"status.edited": "Modificat {date}",
@@ -626,6 +638,7 @@
"upload_modal.preview_label": "Apercebut ({ratio})",
"upload_progress.label": "Mandadís…",
"upload_progress.processing": "Tractament…",
+ "username.taken": "Aqueste nom d’utilizaire es pres. Ensajatz-ne un autre",
"video.close": "Tampar la vidèo",
"video.download": "Telecargar lo fichièr",
"video.exit_fullscreen": "Sortir plen ecran",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 9983936953..cc8b583120 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -48,7 +48,7 @@
"account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。",
"account.media": "媒體",
"account.mention": "提及 @{name}",
- "account.moved_to": "{name} 現在的新帳號為:",
+ "account.moved_to": "{name} 目前的新帳號為:",
"account.mute": "靜音 @{name}",
"account.mute_notifications_short": "靜音推播通知",
"account.mute_short": "靜音",
@@ -59,7 +59,7 @@
"account.posts": "嘟文",
"account.posts_with_replies": "嘟文與回覆",
"account.report": "檢舉 @{name}",
- "account.requested": "正在等待核准。按一下以取消跟隨請求",
+ "account.requested": "正在等候審核。按一下以取消跟隨請求",
"account.requested_follow": "{name} 要求跟隨您",
"account.share": "分享 @{name} 的個人檔案",
"account.show_reblogs": "顯示來自 @{name} 的嘟文",
@@ -84,7 +84,7 @@
"admin.impact_report.title": "影響總結",
"alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。",
"alert.rate_limited.title": "已限速",
- "alert.unexpected.message": "發生了非預期的錯誤。",
+ "alert.unexpected.message": "發生非預期的錯誤。",
"alert.unexpected.title": "哎呀!",
"announcement.announcement": "公告",
"attachments_list.unprocessed": "(未經處理)",
@@ -241,7 +241,7 @@
"empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。",
"empty_column.hashtag": "這個主題標籤下什麼也沒有。",
"empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!",
- "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出了新的嘟文時,它們將顯示於此。",
+ "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。",
"empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。",
"empty_column.mutes": "您尚未靜音任何使用者。",
"empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。",
@@ -303,8 +303,8 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者",
"hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
"hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
- "hashtag.follow": "追蹤主題標籤",
- "hashtag.unfollow": "取消追蹤主題標籤",
+ "hashtag.follow": "跟隨主題標籤",
+ "hashtag.unfollow": "取消跟隨主題標籤",
"hashtags.and_other": "…及其他 {count, plural, other {# 個}}",
"home.actions.go_to_explore": "看看發生什麼新鮮事",
"home.actions.go_to_suggestions": "尋找一些人來跟隨",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index ecfdd2c949..9fc59dd7ff 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -43,9 +43,7 @@ import {
COMPOSE_RESET,
COMPOSE_POLL_ADD,
COMPOSE_POLL_REMOVE,
- COMPOSE_POLL_OPTION_ADD,
COMPOSE_POLL_OPTION_CHANGE,
- COMPOSE_POLL_OPTION_REMOVE,
COMPOSE_POLL_SETTINGS_CHANGE,
COMPOSE_CIRCLE_CHANGE,
INIT_MEDIA_EDIT_MODAL,
@@ -365,6 +363,18 @@ const updateSuggestionTags = (state, token) => {
});
};
+const updatePoll = (state, index, value) => state.updateIn(['poll', 'options'], options => {
+ const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
+
+ if (tmp.size === 0) {
+ return tmp.push('').push('');
+ } else if (tmp.size < 8) {
+ return tmp.push('');
+ }
+
+ return tmp;
+});
+
export default function compose(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
@@ -635,12 +645,8 @@ export default function compose(state = initialState, action) {
return state.set('poll', initialPoll);
case COMPOSE_POLL_REMOVE:
return state.set('poll', null);
- case COMPOSE_POLL_OPTION_ADD:
- return state.updateIn(['poll', 'options'], options => options.push(action.title));
case COMPOSE_POLL_OPTION_CHANGE:
- return state.setIn(['poll', 'options', action.index], action.title);
- case COMPOSE_POLL_OPTION_REMOVE:
- return state.updateIn(['poll', 'options'], options => options.delete(action.index));
+ return updatePoll(state, action.index, action.title);
case COMPOSE_POLL_SETTINGS_CHANGE:
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
case COMPOSE_CIRCLE_CHANGE:
diff --git a/app/javascript/material-icons/400-24px/bar_chart_4_bars-fill.svg b/app/javascript/material-icons/400-24px/bar_chart_4_bars-fill.svg
new file mode 100644
index 0000000000..63215a3e09
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/bar_chart_4_bars-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/bar_chart_4_bars.svg b/app/javascript/material-icons/400-24px/bar_chart_4_bars.svg
new file mode 100644
index 0000000000..63215a3e09
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/bar_chart_4_bars.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/markdown-fill.svg b/app/javascript/material-icons/400-24px/markdown-fill.svg
new file mode 100644
index 0000000000..18a0670518
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/markdown-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/markdown.svg b/app/javascript/material-icons/400-24px/markdown.svg
new file mode 100644
index 0000000000..3396c2f99a
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/photo_library-fill.svg b/app/javascript/material-icons/400-24px/photo_library-fill.svg
new file mode 100644
index 0000000000..e68aec8321
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/photo_library-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/photo_library.svg b/app/javascript/material-icons/400-24px/photo_library.svg
new file mode 100644
index 0000000000..3b22224b83
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/photo_library.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/quiet_time-fill.svg b/app/javascript/material-icons/400-24px/quiet_time-fill.svg
new file mode 100644
index 0000000000..aed5740db3
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/quiet_time-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/quiet_time.svg b/app/javascript/material-icons/400-24px/quiet_time.svg
new file mode 100644
index 0000000000..552da6658d
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/quiet_time.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/translate-fill.svg b/app/javascript/material-icons/400-24px/translate-fill.svg
new file mode 100644
index 0000000000..ecaaf37f1e
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/translate-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/translate.svg b/app/javascript/material-icons/400-24px/translate.svg
new file mode 100644
index 0000000000..ecaaf37f1e
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/translate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/warning-fill.svg b/app/javascript/material-icons/400-24px/warning-fill.svg
new file mode 100644
index 0000000000..c3727d3f57
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/warning-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/warning.svg b/app/javascript/material-icons/400-24px/warning.svg
new file mode 100644
index 0000000000..238299e606
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/packs/share.jsx b/app/javascript/packs/share.jsx
index 0f3b84549d..7b5723091c 100644
--- a/app/javascript/packs/share.jsx
+++ b/app/javascript/packs/share.jsx
@@ -13,10 +13,12 @@ function loaded() {
if (mountNode) {
const attr = mountNode.getAttribute('data-props');
- if(!attr) return;
+
+ if (!attr) return;
const props = JSON.parse(attr);
const root = createRoot(mountNode);
+
root.render(
);
}
}
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
index 1c2386f02d..ae607f484a 100644
--- a/app/javascript/styles/contrast/diff.scss
+++ b/app/javascript/styles/contrast/diff.scss
@@ -1,20 +1,7 @@
-.compose-form {
- .compose-form__modifiers {
- .compose-form__upload {
- &-description {
- input {
- &::placeholder {
- opacity: 1;
- }
- }
- }
- }
- }
-}
-
.status__content a,
-.link-footer a,
.reply-indicator__content a,
+.edit-indicator__content a,
+.link-footer a,
.status__content__read-more-button,
.status__content__translate-button {
text-decoration: underline;
@@ -42,7 +29,9 @@
}
}
-.status__content a {
+.status__content a,
+.reply-indicator__content a,
+.edit-indicator__content a {
color: $highlight-text-color;
}
@@ -50,24 +39,10 @@
color: $darker-text-color;
}
-.compose-form__poll-wrapper .button.button-secondary,
-.compose-form .autosuggest-textarea__textarea::placeholder,
-.compose-form .spoiler-input__input::placeholder,
-.report-dialog-modal__textarea::placeholder,
-.language-dropdown__dropdown__results__item__common-name,
-.compose-form .icon-button {
+.report-dialog-modal__textarea::placeholder {
color: $inverted-text-color;
}
-.text-icon-button.active {
- color: $ui-highlight-color;
-}
-
-.language-dropdown__dropdown__results__item.active {
- background: $ui-highlight-color;
- font-weight: 500;
-}
-
.link-button:disabled {
cursor: not-allowed;
diff --git a/app/javascript/styles/full-dark/diff.scss b/app/javascript/styles/full-dark/diff.scss
index 1ff2a6932c..8a9f90ce02 100644
--- a/app/javascript/styles/full-dark/diff.scss
+++ b/app/javascript/styles/full-dark/diff.scss
@@ -33,9 +33,6 @@ textarea {
}
.compose-form .compose-form__warning,
-.reply-indicator__content,
-.reply-indicator__display-name,
-.reply-indicator__cancel,
.autosuggest-textarea__suggestions__item {
color: $ui-base-color;
}
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 93e34804be..3c75854d9b 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -145,10 +145,6 @@ html {
}
}
-.compose-form__autosuggest-wrapper,
-.poll__option input[type='text'],
-.compose-form .spoiler-input__input,
-.compose-form__poll-wrapper select,
.search__input,
.setting-text,
.report-dialog-modal__textarea,
@@ -172,28 +168,11 @@ html {
border-bottom: 0;
}
-.compose-form__poll-wrapper select {
- background: $simple-background-color
- url("data:image/svg+xml;utf8,
")
- no-repeat right 8px center / auto 16px;
-}
-
-.compose-form__poll-wrapper,
-.compose-form__poll-wrapper .poll__footer {
- border-top-color: lighten($ui-base-color, 8%);
-}
-
.notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
-.compose-form .compose-form__buttons-wrapper {
- background: $ui-base-color;
- border: 1px solid lighten($ui-base-color, 8%);
- border-top: 0;
-}
-
.drawer__header,
.drawer__inner {
background: $white;
@@ -206,52 +185,6 @@ html {
no-repeat bottom / 100% auto;
}
-// Change the colors used in compose-form
-.compose-form {
- .compose-form__modifiers {
- .compose-form__upload__actions .icon-button,
- .compose-form__upload__warning .icon-button {
- color: lighten($white, 7%);
-
- &:active,
- &:focus,
- &:hover {
- color: $white;
- }
- }
- }
-
- .compose-form__buttons-wrapper {
- background: darken($ui-base-color, 6%);
- }
-
- .autosuggest-textarea__suggestions {
- background: darken($ui-base-color, 6%);
- }
-
- .autosuggest-textarea__suggestions__item {
- &:hover,
- &:focus,
- &:active,
- &.selected {
- background: lighten($ui-base-color, 4%);
- }
- }
-}
-
-.emoji-mart-bar {
- border-color: lighten($ui-base-color, 4%);
-
- &:first-child {
- background: darken($ui-base-color, 6%);
- }
-}
-
-.emoji-mart-search input {
- background: rgba($ui-base-color, 0.3);
- border-color: $ui-base-color;
-}
-
.upload-progress__backdrop {
background: $ui-base-color;
}
@@ -283,55 +216,11 @@ html {
background: $ui-base-color;
}
-.privacy-dropdown.active .privacy-dropdown__value.active .icon-button,
-.expiration-dropdown.active .expiration-dropdown__value.active .icon-button {
- color: $white;
-}
-
.account-gallery__item a {
background-color: $ui-base-color;
}
-// Change the colors used in the dropdown menu
-.dropdown-menu {
- background: $white;
-
- &__arrow::before {
- background-color: $white;
- }
-
- &__item {
- color: $darker-text-color;
-
- &--dangerous {
- color: $error-value-color;
- }
-
- a,
- button {
- background: $white;
- }
- }
-}
-
// Change the text colors on inverted background
-.privacy-dropdown__option.active,
-.privacy-dropdown__option:hover,
-.privacy-dropdown__option.active .privacy-dropdown__option__content,
-.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
-.privacy-dropdown__option:hover .privacy-dropdown__option__content,
-.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
-.expiration-dropdown__option.active,
-.expiration-dropdown__option:hover,
-.expiration-dropdown__option.active .expiration-dropdown__option__content,
-.expiration-dropdown__option.active
- .expiration-dropdown__option__content
- strong,
-.expiration-dropdown__option:hover .expiration-dropdown__option__content,
-.expiration-dropdown__option:hover .expiration-dropdown__option__content strong,
-.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active,
-.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus,
-.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover,
.actions-modal ul li:not(:empty) a.active,
.actions-modal ul li:not(:empty) a.active button,
.actions-modal ul li:not(:empty) a:active,
@@ -340,7 +229,6 @@ html {
.actions-modal ul li:not(:empty) a:focus button,
.actions-modal ul li:not(:empty) a:hover,
.actions-modal ul li:not(:empty) a:hover button,
-.language-dropdown__dropdown__results__item.active,
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
.simple_form .block-button,
.simple_form .button,
@@ -348,19 +236,6 @@ html {
color: $white;
}
-.language-dropdown__dropdown__results__item
- .language-dropdown__dropdown__results__item__common-name {
- color: lighten($ui-base-color, 8%);
-}
-
-.language-dropdown__dropdown__results__item.active
- .language-dropdown__dropdown__results__item__common-name {
- color: darken($ui-base-color, 12%);
-}
-
-.dropdown-menu__separator,
-.dropdown-menu__item.edited-timestamp__history__item,
-.dropdown-menu__container__header,
.compare-history-modal .report-modal__target,
.report-dialog-modal .poll__option.dialog-option {
border-bottom-color: lighten($ui-base-color, 4%);
@@ -394,10 +269,7 @@ html {
.reactions-bar__item:hover,
.reactions-bar__item:focus,
-.reactions-bar__item:active,
-.language-dropdown__dropdown__results__item:hover,
-.language-dropdown__dropdown__results__item:focus,
-.language-dropdown__dropdown__results__item:active {
+.reactions-bar__item:active {
background-color: $ui-base-color;
}
@@ -640,11 +512,6 @@ html {
}
}
-.reply-indicator {
- background: transparent;
- border: 1px solid lighten($ui-base-color, 8%);
-}
-
.status__content,
.reply-indicator__content {
a {
@@ -684,3 +551,30 @@ html {
background-color: rgba($ui-highlight-color, 0.15);
}
}
+
+.compose-form__actions .icon-button.active,
+.dropdown-button.active,
+.privacy-dropdown__option.active,
+.privacy-dropdown__option:focus,
+.language-dropdown__dropdown__results__item:focus,
+.language-dropdown__dropdown__results__item.active,
+.privacy-dropdown__option:focus .privacy-dropdown__option__content,
+.privacy-dropdown__option:focus .privacy-dropdown__option__content strong,
+.privacy-dropdown__option.active .privacy-dropdown__option__content,
+.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
+.language-dropdown__dropdown__results__item:focus
+ .language-dropdown__dropdown__results__item__common-name,
+.language-dropdown__dropdown__results__item.active
+ .language-dropdown__dropdown__results__item__common-name {
+ color: $white;
+}
+
+.compose-form .spoiler-input__input {
+ color: lighten($ui-highlight-color, 8%);
+}
+
+.compose-form .autosuggest-textarea__textarea,
+.compose-form__highlightable,
+.poll__option input[type='text'] {
+ background: darken($ui-base-color, 10%);
+}
diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss
index 4573b0c331..a1cccc07a2 100644
--- a/app/javascript/styles/mastodon-light/variables.scss
+++ b/app/javascript/styles/mastodon-light/variables.scss
@@ -5,7 +5,7 @@ $white: #ffffff;
$classic-base-color: #282c37;
$classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
-$classic-highlight-color: #858afa;
+$classic-highlight-color: #6364ff;
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
@@ -37,7 +37,7 @@ $ui-button-tertiary-border-color: $blurple-500 !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
-$highlight-text-color: darken($ui-highlight-color, 8%) !default;
+$highlight-text-color: $ui-highlight-color !default;
$dark-text-color: #444b5d;
$action-button-color: #606984;
@@ -58,3 +58,8 @@ $account-background-color: $white !default;
}
$emojis-requiring-inversion: 'chains';
+
+.theme-mastodon-light {
+ --dropdown-border-color: #d9e1e8;
+ --dropdown-background-color: #fff;
+}
diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index dcfab6bd01..d7f8586dd2 100644
--- a/app/javascript/styles/mastodon/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
@@ -15,13 +15,14 @@
outline: 0;
box-sizing: border-box;
width: 100%;
- border: 0;
box-shadow: none;
font-family: inherit;
background: $ui-base-color;
color: $darker-text-color;
border-radius: 4px;
- font-size: 14px;
+ border: 1px solid lighten($ui-base-color, 8%);
+ font-size: 17px;
+ line-height: normal;
margin: 0;
}
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index b0c26362c8..e29bf36067 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -1321,6 +1321,9 @@ a.sparkline {
&__label {
padding: 15px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
}
&__rules {
@@ -1331,6 +1334,9 @@ a.sparkline {
&__rule {
cursor: pointer;
padding: 15px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
}
}
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 6714b24268..28dad81da5 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -8,7 +8,7 @@
body {
font-family: $font-sans-serif, sans-serif;
- background: darken($ui-base-color, 7%);
+ background: darken($ui-base-color, 8%);
font-size: 13px;
line-height: 18px;
font-weight: 400;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 7bf519486c..0b0a95fd99 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -368,46 +368,155 @@ body > [data-popper-placement] {
}
}
-.compose-form {
- padding: 15px;
+.autosuggest-textarea {
+ &__textarea {
+ background: transparent;
+ min-height: 100px;
+ padding-bottom: 0;
+ resize: none;
+ scrollbar-color: initial;
- &__sensitive-button {
- padding: 10px;
- padding-top: 0;
- font-size: 14px;
- font-weight: 500;
-
- &.active {
- color: $highlight-text-color;
- }
-
- input[type='checkbox'] {
- appearance: none;
- display: inline-block;
- position: relative;
- border: 1px solid $ui-primary-color;
- box-sizing: border-box;
- width: 18px;
- height: 18px;
- flex: 0 0 auto;
- margin-inline-end: 10px;
- top: -1px;
- border-radius: 4px;
- vertical-align: middle;
- cursor: inherit;
-
- &:checked {
- border-color: $highlight-text-color;
- background: $highlight-text-color
- url("data:image/svg+xml;utf8,
")
- center center no-repeat;
- }
+ &::-webkit-scrollbar {
+ all: unset;
}
}
- .compose-form__warning {
+ &__suggestions {
+ box-shadow: var(--dropdown-shadow);
+ background: $ui-base-color;
+ border: 1px solid lighten($ui-base-color, 14%);
+ border-radius: 0 0 4px 4px;
+ color: $secondary-text-color;
+ font-size: 14px;
+ padding: 0;
+
+ &__item {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ height: 48px;
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ color: $secondary-text-color;
+
+ &:last-child {
+ border-radius: 0 0 4px 4px;
+ }
+
+ &:hover,
+ &:focus,
+ &:active,
+ &.selected {
+ background: $ui-highlight-color;
+ color: $primary-text-color;
+
+ .autosuggest-account .display-name__account {
+ color: inherit;
+ }
+ }
+ }
+ }
+}
+
+.autosuggest-account,
+.autosuggest-emoji,
+.autosuggest-hashtag {
+ flex: 1 0 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 12px;
+ padding: 8px 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.autosuggest-account {
+ .display-name {
+ font-weight: 400;
+ display: flex;
+ flex-direction: column;
+ flex: 1 0 0;
+ }
+
+ .display-name__account {
+ display: block;
+ line-height: 16px;
+ font-size: 12px;
+ color: $dark-text-color;
+ }
+}
+
+.autosuggest-hashtag {
+ justify-content: space-between;
+
+ &__name {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__uses {
+ flex: 0 0 auto;
+ text-align: end;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.autosuggest-emoji {
+ &__name {
+ flex: 1 0 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.autosuggest-account .account__avatar,
+.autosuggest-emoji img {
+ display: block;
+ width: 24px;
+ height: 24px;
+ flex: 0 0 auto;
+}
+
+.compose-form {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ .layout-multiple-columns &,
+ .column & {
+ padding: 15px;
+ }
+
+ &__highlightable {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ flex: 0 1 auto;
+ border-radius: 4px;
+ border: 1px solid lighten($ui-base-color, 8%);
+ transition: border-color 300ms linear;
+ min-height: 0;
+ position: relative;
+ background: $ui-base-color;
+ overflow-y: auto;
+
+ &.active {
+ transition: none;
+ border-color: $ui-highlight-color;
+ }
+ }
+
+ &__warning {
color: $inverted-text-color;
- margin-bottom: 10px;
background: $ui-primary-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
@@ -439,39 +548,31 @@ body > [data-popper-placement] {
}
}
- .emoji-picker-dropdown {
- position: absolute;
- top: 0;
- inset-inline-end: 0;
- }
-
- .expiration-dropdown {
- position: absolute;
- top: 32px;
- right: 4px;
- transform: scale(1.2);
- }
-
.compose-form__autosuggest-wrapper {
position: relative;
}
- .autosuggest-textarea,
- .autosuggest-input,
.spoiler-input {
- position: relative;
- width: 100%;
- }
+ display: flex;
+ align-items: stretch;
- .spoiler-input {
- height: 0;
- transform-origin: bottom;
- opacity: 0;
+ &__border {
+ background: url('../images/warning-stripes.svg') repeat-y;
+ width: 5px;
+ flex: 0 0 auto;
- &.spoiler-input--visible {
- height: 36px;
- margin-bottom: 11px;
- opacity: 1;
+ &:first-child {
+ border-start-start-radius: 4px;
+ }
+
+ &:last-child {
+ border-start-end-radius: 4px;
+ }
+ }
+
+ .autosuggest-input {
+ flex: 1 1 auto;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
}
}
@@ -481,273 +582,311 @@ body > [data-popper-placement] {
box-sizing: border-box;
width: 100%;
margin: 0;
- color: $inverted-text-color;
- background: $simple-background-color;
- padding: 10px;
+ color: $secondary-text-color;
+ background: $ui-base-color;
font-family: inherit;
font-size: 14px;
- resize: vertical;
+ padding: 12px;
+ line-height: normal;
border: 0;
outline: 0;
- &::placeholder {
- color: $dark-text-color;
- }
-
&:focus {
outline: 0;
}
-
- @media screen and (width <= 600px) {
- font-size: 16px;
- }
}
.spoiler-input__input {
- border-radius: 4px;
+ padding: 12px 12px - 5px;
+ background: mix($ui-base-color, $ui-highlight-color, 85%);
+ color: $highlight-text-color;
}
- .autosuggest-textarea__textarea {
- min-height: 100px;
- border-radius: 4px 4px 0 0;
- padding-bottom: 0;
- padding-right: 10px + 22px; // Cannot use inline-end because of dir=auto
- resize: none;
- scrollbar-color: initial;
-
- &::-webkit-scrollbar {
- all: unset;
- }
-
- @media screen and (width <= 600px) {
- height: 100px !important; // Prevent auto-resize textarea
- resize: vertical;
- }
- }
-
- .autosuggest-textarea__suggestions-wrapper {
- position: relative;
- height: 0;
- }
-
- .autosuggest-textarea__suggestions {
- box-sizing: border-box;
- display: none;
- position: absolute;
- top: 100%;
- width: 100%;
- z-index: 99;
- box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
- background: $ui-secondary-color;
- border-radius: 0 0 4px 4px;
- color: $inverted-text-color;
- font-size: 14px;
- padding: 6px;
-
- &.autosuggest-textarea__suggestions--visible {
- display: block;
- }
- }
-
- .autosuggest-textarea__suggestions__item {
- padding: 10px;
- cursor: pointer;
- border-radius: 4px;
-
- &:hover,
- &:focus,
- &:active,
- &.selected {
- background: darken($ui-secondary-color, 10%);
- }
- }
-
- .autosuggest-account,
- .autosuggest-emoji,
- .autosuggest-hashtag {
+ &__dropdowns {
display: flex;
- flex-direction: row;
align-items: center;
- justify-content: flex-start;
- line-height: 18px;
- font-size: 14px;
- }
+ gap: 8px;
- .autosuggest-hashtag {
- justify-content: space-between;
+ &__second {
+ margin-top: -8px;
+ }
- &__name {
- flex: 1 1 auto;
+ & > div {
overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- strong {
- font-weight: 500;
- }
-
- &__uses {
- flex: 0 0 auto;
- text-align: end;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
- .autosuggest-account-icon,
- .autosuggest-emoji img {
- display: block;
- margin-inline-end: 8px;
- width: 16px;
- height: 16px;
- }
-
- .autosuggest-account .display-name__account {
- color: $lighter-text-color;
- }
-
- .compose-form__modifiers {
- color: $inverted-text-color;
- font-family: inherit;
- font-size: 14px;
- background: $simple-background-color;
-
- .compose-form__upload-wrapper {
- overflow: hidden;
- }
-
- .compose-form__uploads-wrapper {
display: flex;
- flex-direction: row;
- padding: 5px;
- flex-wrap: wrap;
+ }
+ }
+
+ &__uploads {
+ display: flex;
+ gap: 8px;
+ padding: 0 12px;
+ flex-wrap: wrap;
+ align-self: stretch;
+ align-items: flex-start;
+ align-content: flex-start;
+ justify-content: center;
+ }
+
+ &__upload {
+ flex: 1 1 0;
+ min-width: calc(50% - 8px);
+
+ &__actions {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 8px;
}
- .compose-form__upload {
- flex: 1 1 0;
- min-width: 40%;
- margin: 5px;
-
- &__actions {
- background: linear-gradient(
- 180deg,
- rgba($base-shadow-color, 0.8) 0,
- rgba($base-shadow-color, 0.35) 80%,
- transparent
- );
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- }
-
- .icon-button {
- flex: 0 1 auto;
- color: $secondary-text-color;
- font-size: 14px;
- font-weight: 500;
- padding: 10px;
- font-family: inherit;
-
- .icon {
- width: 15px;
- height: 15px;
- }
-
- &:hover,
- &:focus,
- &:active {
- color: lighten($secondary-text-color, 7%);
- }
- }
-
- &__warning {
- position: absolute;
- z-index: 2;
- bottom: 0;
- inset-inline-start: 0;
- inset-inline-end: 0;
- box-sizing: border-box;
- background: linear-gradient(
- 0deg,
- rgba($base-shadow-color, 0.8) 0,
- rgba($base-shadow-color, 0.35) 80%,
- transparent
- );
- }
+ &__preview {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: 6px;
+ z-index: -1;
+ top: 0;
+ inset-inline-start: 0;
}
- .compose-form__upload-thumbnail {
- border-radius: 4px;
- background-color: $base-shadow-color;
+ &__thumbnail {
+ width: 100%;
+ height: 144px;
+ border-radius: 6px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
- height: 140px;
- width: 100%;
overflow: hidden;
}
+
+ .icon-button {
+ flex: 0 0 auto;
+ color: $white;
+ background: rgba(0, 0, 0, 75%);
+ border-radius: 6px;
+ font-size: 12px;
+ line-height: 16px;
+ font-weight: 500;
+ padding: 4px 8px;
+ font-family: inherit;
+
+ .icon {
+ width: 15px;
+ height: 15px;
+ }
+ }
+
+ .icon-button.compose-form__upload__delete {
+ padding: 3px;
+ border-radius: 50%;
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ &__warning {
+ position: absolute;
+ z-index: 2;
+ bottom: 0;
+ inset-inline-start: 0;
+ inset-inline-end: 0;
+ padding: 8px;
+
+ .icon-button.active {
+ color: #ffbe2e;
+ background: rgba(0, 0, 0, 75%);
+ }
+ }
}
- .compose-form__buttons-wrapper {
- padding: 10px;
- background: darken($simple-background-color, 8%);
- border-radius: 0 0 4px 4px;
+ &__footer {
display: flex;
- justify-content: space-between;
- flex: 0 0 auto;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+ padding-top: 0;
+ }
- .compose-form__buttons {
+ &__submit {
+ display: flex;
+ align-items: center;
+ flex: 1 1 auto;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex: 1 1 auto;
+
+ & > div {
display: flex;
- gap: 2px;
-
- .icon-button {
- height: 100%;
- }
.searchability .icon-button {
height: 27px;
}
-
- .compose-form__upload-button-icon {
- line-height: 27px;
- }
-
- .compose-form__sensitive-button {
- display: none;
-
- &.compose-form__sensitive-button--visible {
- display: block;
- }
-
- .compose-form__sensitive-button__icon {
- line-height: 27px;
- }
- }
}
- .icon-button,
- .text-icon-button {
- box-sizing: content-box;
- padding: 0 3px;
+ .icon-button {
+ padding: 3px;
}
- .character-counter__wrapper {
- align-self: center;
- margin-inline-end: 4px;
+ .icon-button .icon {
+ width: 18px;
+ height: 18px;
}
}
- .compose-form__publish {
+ &__actions {
display: flex;
- justify-content: flex-end;
- min-width: 0;
+ align-items: center;
flex: 0 0 auto;
+ gap: 12px;
+ flex-wrap: wrap;
- .compose-form__publish-button-wrapper {
- padding-top: 15px;
+ .button {
+ display: block; // Otherwise text-ellipsis doesn't work
+ font-size: 14px;
+ line-height: normal;
+ font-weight: 700;
+ flex: 1 1 auto;
+ padding: 5px 12px;
+ border-radius: 4px;
}
+
+ .icon-button {
+ box-sizing: content-box;
+ color: $highlight-text-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: $highlight-text-color;
+ }
+
+ &.disabled {
+ color: $highlight-text-color;
+ opacity: 0.5;
+ }
+
+ &.active {
+ background: $ui-highlight-color;
+ color: $primary-text-color;
+ }
+ }
+ }
+
+ &__poll {
+ display: flex;
+ flex-direction: column;
+ align-self: stretch;
+ gap: 8px;
+
+ .poll__option {
+ padding: 0 12px;
+ gap: 8px;
+
+ &.empty:not(:focus-within) {
+ opacity: 0.5;
+ }
+ }
+
+ .poll__input {
+ width: 17px;
+ height: 17px;
+ border-color: $darker-text-color;
+ }
+
+ &__footer {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding-inline-start: 37px;
+ padding-inline-end: 40px;
+
+ &__sep {
+ width: 1px;
+ height: 22px;
+ background: lighten($ui-base-color, 8%);
+ flex: 0 0 auto;
+ }
+ }
+
+ &__select {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ flex: 1 1 auto;
+ min-width: 0;
+
+ &__label {
+ flex: 0 0 auto;
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 16px;
+ letter-spacing: 0.5px;
+ color: $darker-text-color;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &__value {
+ flex: 0 0 auto;
+ appearance: none;
+ background: transparent;
+ border: none;
+ padding: 0;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.1px;
+ color: $highlight-text-color;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+.dropdown-button {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: transparent;
+ color: $highlight-text-color;
+ border-radius: 6px;
+ border: 1px solid $highlight-text-color;
+ padding: 4px 8px;
+ font-size: 13px;
+ line-height: normal;
+ font-weight: 400;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ .icon {
+ width: 15px;
+ height: 15px;
+ flex: 0 0 auto;
+ }
+
+ &__label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1 1 auto;
+ }
+
+ &.active {
+ background: $ui-highlight-color;
+ border-color: $ui-highlight-color;
+ color: $primary-text-color;
}
}
@@ -755,11 +894,14 @@ body > [data-popper-placement] {
cursor: default;
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
- font-weight: 600;
- color: $lighter-text-color;
+ font-weight: 400;
+ line-height: normal;
+ color: $darker-text-color;
+ flex: 1 0 auto;
+ text-align: end;
&.character-counter--over {
- color: $warning-red;
+ color: $error-red;
}
}
@@ -806,41 +948,6 @@ body > [data-popper-placement] {
}
}
-.reply-indicator {
- border-radius: 4px;
- margin-bottom: 10px;
- background: $ui-primary-color;
- padding: 10px;
- min-height: 23px;
- overflow-y: auto;
- flex: 0 2 auto;
-}
-
-.reply-indicator__header {
- margin-bottom: 5px;
- overflow: hidden;
-}
-
-.reply-indicator__cancel {
- float: right;
- line-height: 24px;
-}
-
-.reply-indicator__display-name {
- color: $inverted-text-color;
- display: block;
- max-width: 100%;
- line-height: 24px;
- overflow: hidden;
- padding-inline-end: 25px;
- text-decoration: none;
-}
-
-.reply-indicator__display-avatar {
- float: left;
- margin-inline-end: 5px;
-}
-
.status__content--with-action {
cursor: pointer;
}
@@ -850,14 +957,15 @@ body > [data-popper-placement] {
}
.status__content,
+.edit-indicator__content,
.reply-indicator__content {
position: relative;
- font-size: 15px;
- line-height: 22px;
word-wrap: break-word;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
+ font-size: 15px;
+ line-height: 22px;
padding-top: 2px;
color: $primary-text-color;
@@ -943,6 +1051,174 @@ body > [data-popper-placement] {
}
}
+.reply-indicator {
+ display: grid;
+ grid-template-columns: 46px minmax(0, 1fr);
+ grid-template-rows: 46px max-content;
+ gap: 0 10px;
+
+ .detailed-status__display-name {
+ margin-bottom: 4px;
+ }
+
+ .detailed-status__display-avatar {
+ grid-column-start: 1;
+ grid-row-start: 1;
+ grid-row-end: span 1;
+ }
+
+ &__main {
+ grid-column-start: 2;
+ grid-row-start: 1;
+ grid-row-end: span 2;
+ }
+
+ .display-name {
+ font-size: 14px;
+ line-height: 16px;
+
+ &__account {
+ display: none;
+ }
+ }
+
+ &__line {
+ grid-column-start: 1;
+ grid-row-start: 2;
+ grid-row-end: span 1;
+ position: relative;
+
+ &::before {
+ display: block;
+ content: '';
+ position: absolute;
+ inset-inline-start: 50%;
+ top: 4px;
+ transform: translateX(-50%);
+ background: lighten($ui-base-color, 8%);
+ width: 2px;
+ height: calc(100% + 32px - 8px); // Account for gap to next element
+ }
+ }
+
+ &__content {
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ padding: 0;
+ max-height: 4 * 20px;
+ overflow: hidden;
+ color: $darker-text-color;
+ }
+
+ &__attachments {
+ margin-top: 4px;
+ color: $darker-text-color;
+ font-size: 12px;
+ line-height: 16px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ }
+ }
+}
+
+.edit-indicator {
+ border-radius: 4px 4px 0 0;
+ background: lighten($ui-base-color, 4%);
+ padding: 12px;
+ overflow-y: auto;
+ flex: 0 0 auto;
+ border-bottom: 0.5px solid lighten($ui-base-color, 8%);
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: $darker-text-color;
+ font-size: 12px;
+ line-height: 16px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__cancel {
+ display: flex;
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ &__display-name {
+ display: flex;
+ gap: 4px;
+
+ a {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ &__content {
+ color: $secondary-text-color;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ padding-top: 0 !important;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ max-height: 4 * 20px;
+ overflow: hidden;
+
+ a {
+ color: $highlight-text-color;
+ }
+ }
+
+ &__attachments {
+ color: $darker-text-color;
+ font-size: 12px;
+ line-height: 16px;
+ opacity: 0.75;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ }
+ }
+}
+
+.edit-indicator__content,
+.reply-indicator__content {
+ .emojione {
+ width: 18px;
+ height: 18px;
+ margin: -3px 0 0;
+ }
+}
+
.announcements__item__content {
word-wrap: break-word;
overflow-y: auto;
@@ -1526,15 +1802,6 @@ body > [data-popper-placement] {
line-height: 18px;
}
-.reply-indicator__content {
- color: $inverted-text-color;
- font-size: 14px;
-
- a {
- color: $lighter-text-color;
- }
-}
-
.domain {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
@@ -1749,7 +2016,6 @@ a .account__avatar {
}
.status__display-name,
-.reply-indicator__display-name,
.detailed-status__display-name,
a.account__display-name {
&:hover .display-name strong {
@@ -2017,57 +2283,45 @@ a.account__display-name {
}
.navigation-bar {
- padding: 15px;
display: flex;
align-items: center;
flex-shrink: 0;
cursor: default;
gap: 10px;
- color: $darker-text-color;
- strong {
- color: $secondary-text-color;
+ .column > & {
+ padding: 15px;
}
- a {
- color: inherit;
- text-decoration: none;
- }
+ .account {
+ border-bottom: 0;
+ padding: 0;
+ flex: 1 1 auto;
+ min-width: 0;
- .navigation-bar__actions {
- position: relative;
+ &__display-name {
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0.15px;
+ font-weight: 500;
- .compose__action-bar .icon-button {
- pointer-events: auto;
- transform: scale(1, 1) translate(0, 0);
- opacity: 1;
-
- .icon {
- width: 24px;
- height: 24px;
+ .display-name__account {
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.1px;
}
}
}
-}
-.navigation-bar__profile {
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- line-height: 20px;
-}
+ .icon-button {
+ padding: 8px;
+ color: $secondary-text-color;
+ }
-.navigation-bar__profile-account {
- display: inline;
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.navigation-bar__profile-edit {
- display: inline;
- color: inherit;
- text-decoration: none;
+ .icon-button .icon {
+ width: 24px;
+ height: 24px;
+ }
}
.dropdown-animation {
@@ -2245,6 +2499,7 @@ a.account__display-name {
&__panels {
display: flex;
justify-content: center;
+ gap: 16px;
width: 100%;
height: 100%;
min-height: 100vh;
@@ -2277,7 +2532,6 @@ a.account__display-name {
flex-direction: column;
@media screen and (min-width: $no-gap-breakpoint) {
- padding: 0 10px;
max-width: 600px;
}
}
@@ -2537,6 +2791,7 @@ $ui-header-height: 55px;
.columns-area__panels {
min-height: calc(100vh - $ui-header-height);
+ gap: 0;
}
.columns-area__panels__pane--navigational {
@@ -2936,21 +3191,6 @@ $ui-header-height: 55px;
}
}
-.compose-form__highlightable {
- display: flex;
- flex-direction: column;
- flex: 0 1 auto;
- border-radius: 4px;
- transition: box-shadow 300ms linear;
- min-height: 0;
- position: relative;
-
- &.active {
- transition: none;
- box-shadow: 0 0 0 6px rgba(lighten($highlight-text-color, 8%), 0.7);
- }
-}
-
.compose-panel {
width: 285px;
margin-top: 10px;
@@ -2979,32 +3219,9 @@ $ui-header-height: 55px;
}
}
- .navigation-bar {
- flex: 0 1 48px;
- }
-
.compose-form {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 310px;
- padding-bottom: 71px;
- margin-bottom: -71px;
- }
-
- .compose-form__autosuggest-wrapper {
- overflow-y: auto;
- background-color: $white;
- border-radius: 4px 4px 0 0;
- flex: 0 1 auto;
- }
-
- .autosuggest-textarea__textarea {
- overflow-y: hidden;
- }
-
- .compose-form__upload-thumbnail {
- height: 80px;
+ flex: 1 1 auto;
+ min-height: 0;
}
}
@@ -3024,6 +3241,10 @@ $ui-header-height: 55px;
height: 30px;
width: auto;
}
+
+ &__logo {
+ margin-bottom: 12px;
+ }
}
.navigation-panel,
@@ -3055,7 +3276,7 @@ $ui-header-height: 55px;
position: absolute;
top: 0;
inset-inline-start: 0;
- background: $ui-base-color;
+ background: darken($ui-base-color, 4%);
box-sizing: border-box;
padding: 0;
display: flex;
@@ -3071,7 +3292,7 @@ $ui-header-height: 55px;
}
.drawer__inner__mastodon {
- background: $ui-base-color
+ background: darken($ui-base-color, 4%)
url('data:image/svg+xml;utf8,
')
no-repeat bottom / 100% auto;
flex: 1;
@@ -3093,24 +3314,20 @@ $ui-header-height: 55px;
}
}
-.pseudo-drawer {
- background: lighten($ui-base-color, 13%);
- font-size: 13px;
- text-align: start;
-}
-
.drawer__header {
flex: 0 0 auto;
font-size: 16px;
- background: $ui-base-color;
+ background: darken($ui-base-color, 4%);
margin-bottom: 10px;
display: flex;
flex-direction: row;
border-radius: 4px;
overflow: hidden;
- a:hover {
- background: lighten($ui-base-color, 3%);
+ a:hover,
+ a:focus,
+ a:active {
+ background: $ui-base-color;
}
}
@@ -3322,7 +3539,7 @@ $ui-header-height: 55px;
&--transparent {
background: transparent;
- color: $ui-secondary-color;
+ color: $secondary-text-color;
&:hover,
&:focus,
@@ -4786,10 +5003,7 @@ a.status-card {
}
.emoji-picker-dropdown__menu {
- background: $simple-background-color;
position: relative;
- box-shadow: var(--dropdown-shadow);
- border-radius: 4px;
margin-top: 5px;
z-index: 2;
@@ -4812,11 +5026,12 @@ a.status-card {
.emoji-picker-dropdown__modifiers__menu {
position: absolute;
z-index: 4;
- top: -4px;
- inset-inline-start: -8px;
- background: $simple-background-color;
+ top: -5px;
+ inset-inline-start: -9px;
+ background: var(--dropdown-background-color);
+ border: 1px solid var(--dropdown-border-color);
border-radius: 4px;
- box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
+ box-shadow: var(--dropdown-shadow);
overflow: hidden;
button {
@@ -4829,7 +5044,7 @@ a.status-card {
&:hover,
&:focus,
&:active {
- background: rgba($ui-secondary-color, 0.4);
+ background: var(--dropdown-border-color);
}
}
@@ -4898,15 +5113,17 @@ a.status-card {
}
.upload-progress {
- padding: 10px;
- color: $lighter-text-color;
+ color: $darker-text-color;
overflow: hidden;
display: flex;
- gap: 10px;
+ gap: 8px;
+ align-items: center;
+ padding: 0 12px;
.icon {
width: 24px;
height: 24px;
+ color: $ui-highlight-color;
}
span {
@@ -4925,7 +5142,7 @@ a.status-card {
width: 100%;
height: 6px;
border-radius: 6px;
- background: darken($simple-background-color, 8%);
+ background: darken($ui-base-color, 8%);
position: relative;
margin-top: 5px;
}
@@ -4980,12 +5197,15 @@ a.status-card {
}
.privacy-dropdown__dropdown,
-.expiration-dropdown__dropdown {
- background: $simple-background-color;
+.language-dropdown__dropdown {
box-shadow: var(--dropdown-shadow);
+ background: var(--dropdown-background-color);
+ border: 1px solid var(--dropdown-border-color);
+ padding: 4px;
border-radius: 4px;
overflow: hidden;
z-index: 2;
+ width: 300px;
&.top {
transform-origin: 50% 100%;
@@ -4996,8 +5216,7 @@ a.status-card {
}
}
-.modal-root__container .privacy-dropdown,
-.modal-root__container .expiration-dropdown {
+.modal-root__container .privacy-dropdown {
flex-grow: 0;
}
@@ -5007,30 +5226,42 @@ a.status-card {
z-index: 9999;
}
-.privacy-dropdown__option,
-.expiration-dropdown__option {
- color: $inverted-text-color;
- padding: 10px;
+.privacy-dropdown__option {
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ padding: 8px 12px;
cursor: pointer;
display: flex;
+ align-items: center;
+ gap: 12px;
+ border-radius: 4px;
+ color: $primary-text-color;
&:hover,
+ &:active {
+ background: var(--dropdown-border-color);
+ }
+
+ &:focus,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
outline: 0;
- .privacy-dropdown__option__content {
+ .privacy-dropdown__option__content,
+ .privacy-dropdown__option__content strong,
+ .privacy-dropdown__option__additional {
color: $primary-text-color;
-
- strong {
- color: $primary-text-color;
- }
}
}
- &.active:hover {
- background: lighten($ui-highlight-color, 4%);
+ &__additional {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $darker-text-color;
+ cursor: help;
}
}
@@ -5038,17 +5269,16 @@ a.status-card {
display: flex;
align-items: center;
justify-content: center;
- margin-inline-end: 10px;
}
.privacy-dropdown__option__content {
flex: 1 1 auto;
- color: $lighter-text-color;
+ color: $darker-text-color;
strong {
+ color: $primary-text-color;
font-weight: 500;
display: block;
- color: $inverted-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
@@ -5058,21 +5288,17 @@ a.status-card {
}
}
-.privacy-dropdown.active,
-.expiration-dropdown.active {
- .privacy-dropdown__value,
- .expiration-dropdown__value {
+.privacy-dropdown.active {
+ .privacy-dropdown__value {
background: $simple-background-color;
border-radius: 4px 4px 0 0;
}
- &.top .privacy-dropdown__value,
- &.top .expiration-dropdown__value {
+ &.top .privacy-dropdown__value {
border-radius: 0 0 4px 4px;
}
- .privacy-dropdown__dropdown,
- .expiration-dropdown__dropdown {
+ .privacy-dropdown__dropdown {
display: block;
box-shadow: var(--dropdown-shadow);
}
@@ -5094,64 +5320,78 @@ a.status-card {
.language-dropdown {
&__dropdown {
- background: $simple-background-color;
- box-shadow: var(--dropdown-shadow);
- border-radius: 4px;
- overflow: hidden;
- z-index: 2;
-
- &.top {
- transform-origin: 50% 100%;
- }
-
- &.bottom {
- transform-origin: 50% 0;
- }
+ padding: 0;
.emoji-mart-search {
- padding-inline-end: 10px;
+ padding: 10px;
+ background: var(--dropdown-background-color);
+
+ input {
+ padding: 8px 12px;
+ background: $ui-base-color;
+ border: 1px solid lighten($ui-base-color, 8%);
+ color: $darker-text-color;
+
+ @media screen and (width <= 600px) {
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0.5px;
+ }
+ }
}
.emoji-mart-search-icon {
- inset-inline-end: 10px + 5px;
+ inset-inline-end: 15px;
+ opacity: 1;
+ color: $darker-text-color;
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ }
+
+ &:disabled {
+ opacity: 0.38;
+ }
}
.emoji-mart-scroll {
padding: 0 10px 10px;
+ background: var(--dropdown-background-color);
}
&__results {
&__item {
cursor: pointer;
- color: $inverted-text-color;
+ color: $primary-text-color;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
font-weight: 500;
- padding: 10px;
+ padding: 8px 12px;
border-radius: 4px;
- display: flex;
- gap: 6px;
- align-items: center;
-
- &:focus,
- &:active,
- &:hover {
- background: $ui-secondary-color;
- }
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
&__common-name {
color: $darker-text-color;
+ font-weight: 400;
}
+ &:active,
+ &:hover {
+ background: var(--dropdown-border-color);
+ }
+
+ &:focus,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
outline: 0;
.language-dropdown__dropdown__results__item__common-name {
- color: $secondary-text-color;
- }
-
- &:hover {
- background: lighten($ui-highlight-color, 4%);
+ color: $primary-text-color;
}
}
}
@@ -5160,9 +5400,13 @@ a.status-card {
}
.search {
- margin-bottom: 10px;
+ margin-bottom: 32px;
position: relative;
+ .layout-multiple-columns & {
+ margin-bottom: 10px;
+ }
+
&__popout {
box-sizing: border-box;
display: none;
@@ -5171,6 +5415,7 @@ a.status-card {
margin-top: -2px;
width: 100%;
background: $ui-base-color;
+ border: 1px solid lighten($ui-base-color, 8%);
border-radius: 0 0 4px 4px;
box-shadow: var(--dropdown-shadow);
z-index: 99;
@@ -5179,7 +5424,7 @@ a.status-card {
h4 {
text-transform: uppercase;
- color: $dark-text-color;
+ color: $darker-text-color;
font-weight: 500;
padding: 0 10px;
margin-bottom: 10px;
@@ -5187,6 +5432,7 @@ a.status-card {
.icon-button {
padding: 0;
+ color: $darker-text-color;
}
.icon {
@@ -5202,7 +5448,7 @@ a.status-card {
}
&__message {
- color: $dark-text-color;
+ color: $darker-text-color;
padding: 0 10px;
}
@@ -5258,6 +5504,10 @@ a.status-card {
}
&.active {
+ .search__input {
+ border-radius: 4px 4px 0 0;
+ }
+
.search__popout {
display: block;
}
@@ -5268,14 +5518,9 @@ a.status-card {
@include search-input;
display: block;
- padding: 15px;
- padding-inline-end: 30px;
- line-height: 18px;
- font-size: 16px;
-
- &::placeholder {
- color: lighten($darker-text-color, 4%);
- }
+ padding: 12px 16px;
+ padding-inline-start: 16px + 15px + 8px;
+ line-height: normal;
&::-moz-focus-inner {
border: 0;
@@ -5286,10 +5531,6 @@ a.status-card {
&:active {
outline: 0 !important;
}
-
- &:focus {
- background: lighten($ui-base-color, 4%);
- }
}
.search__icon {
@@ -5304,21 +5545,21 @@ a.status-card {
.icon {
position: absolute;
- top: 13px;
- inset-inline-end: 10px;
+ top: 12px + 2px;
+ inset-inline-start: 16px - 2px;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
transition-property: transform, opacity;
- width: 24px;
- height: 24px;
- color: $secondary-text-color;
+ width: 20px;
+ height: 20px;
+ color: $darker-text-color;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
- opacity: 0.3;
+ opacity: 1;
}
}
@@ -5333,16 +5574,10 @@ a.status-card {
.icon-times-circle {
transform: rotate(0deg);
- color: $action-button-color;
cursor: pointer;
&.active {
transform: rotate(90deg);
- opacity: 1;
- }
-
- &:hover {
- color: lighten($action-button-color, 7%);
}
}
}
@@ -5949,6 +6184,11 @@ a.status-card {
}
}
+ .dialog-option {
+ align-items: center;
+ gap: 12px;
+ }
+
.dialog-option .poll__input {
border-color: $inverted-text-color;
color: $ui-secondary-color;
@@ -5957,8 +6197,8 @@ a.status-card {
justify-content: center;
svg {
- width: 8px;
- height: auto;
+ width: 15px;
+ height: 15px;
}
&:active,
@@ -7178,90 +7418,6 @@ noscript {
}
}
-@media screen and (width <= 630px) and (height <= 400px) {
- $duration: 400ms;
- $delay: 100ms;
-
- .search {
- will-change: margin-top;
- transition: margin-top $duration $delay;
- }
-
- .navigation-bar {
- will-change: padding-bottom;
- transition: padding-bottom $duration $delay;
- }
-
- .navigation-bar {
- & > a:first-child {
- will-change: margin-top, margin-inline-start, margin-inline-end, width;
- transition:
- margin-top $duration $delay,
- margin-inline-start $duration ($duration + $delay),
- margin-inline-end $duration ($duration + $delay);
- }
-
- & > .navigation-bar__profile-edit {
- will-change: margin-top;
- transition: margin-top $duration $delay;
- }
-
- .navigation-bar__actions {
- & > .icon-button.close {
- will-change: opacity transform;
- transition:
- opacity $duration * 0.5 $delay,
- transform $duration $delay;
- }
-
- & > .compose__action-bar .icon-button {
- will-change: opacity transform;
- transition:
- opacity $duration * 0.5 $delay + $duration * 0.5,
- transform $duration $delay;
- }
- }
- }
-
- .is-composing {
- .search {
- margin-top: -50px;
- }
-
- .navigation-bar {
- padding-bottom: 0;
-
- & > a:first-child {
- margin: -100px 10px 0 -50px;
- }
-
- .navigation-bar__profile {
- padding-top: 2px;
- }
-
- .navigation-bar__profile-edit {
- position: absolute;
- margin-top: -60px;
- }
-
- .navigation-bar__actions {
- .icon-button.close {
- pointer-events: auto;
- opacity: 1;
- transform: scale(1, 1) translate(0, 0);
- bottom: 5px;
- }
-
- .compose__action-bar .icon-button {
- pointer-events: none;
- opacity: 0;
- transform: scale(0, 1) translate(100%, 0);
- }
- }
- }
- }
-}
-
.embed-modal {
width: auto;
max-width: 80vw;
@@ -9358,11 +9514,14 @@ noscript {
.link-footer {
flex: 0 0 auto;
- padding: 10px;
padding-top: 20px;
z-index: 1;
font-size: 13px;
+ .column & {
+ padding: 15px;
+ }
+
p {
color: $dark-text-color;
margin-bottom: 20px;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index b6e995787d..8a472d75b1 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -40,13 +40,12 @@
.compose-form {
width: 400px;
margin: 0 auto;
- padding: 20px 0;
- margin-top: 40px;
+ padding: 10px 0;
+ padding-bottom: 20px;
box-sizing: border-box;
@media screen and (width <= 400px) {
width: 100%;
- margin-top: 0;
padding: 20px;
}
}
@@ -56,13 +55,15 @@
width: 400px;
margin: 0 auto;
display: flex;
- font-size: 13px;
- line-height: 18px;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+ line-height: 20px;
box-sizing: border-box;
padding: 20px 0;
margin-top: 40px;
margin-bottom: 10px;
- border-bottom: 1px solid $ui-base-color;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
@media screen and (width <= 440px) {
width: 100%;
@@ -71,9 +72,9 @@
}
.avatar {
- width: 40px;
- height: 40px;
- margin-inline-end: 10px;
+ width: 48px;
+ height: 48px;
+ flex: 0 0 auto;
img {
width: 100%;
@@ -87,13 +88,14 @@
.name {
flex: 1 1 auto;
color: $secondary-text-color;
- width: calc(100% - 90px);
.username {
display: block;
- font-weight: 500;
+ font-size: 16px;
+ line-height: 24px;
text-overflow: ellipsis;
overflow: hidden;
+ color: $primary-text-color;
}
}
@@ -101,7 +103,7 @@
display: block;
font-size: 32px;
line-height: 40px;
- margin-inline-start: 10px;
+ flex: 0 0 auto;
}
}
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index ba58aec7dc..24cd0fc458 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -1,7 +1,6 @@
.emoji-mart {
font-size: 13px;
display: inline-block;
- color: $inverted-text-color;
&,
* {
@@ -15,13 +14,13 @@
}
.emoji-mart-bar {
- border: 0 solid darken($ui-secondary-color, 8%);
+ border: 0 solid var(--dropdown-border-color);
&:first-child {
border-bottom-width: 1px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
- background: $ui-secondary-color;
+ background: var(--dropdown-border-color);
}
&:last-child {
@@ -36,7 +35,6 @@
display: flex;
justify-content: space-between;
padding: 0 6px;
- color: $lighter-text-color;
line-height: 0;
}
@@ -50,9 +48,10 @@
cursor: pointer;
background: transparent;
border: 0;
+ color: $darker-text-color;
&:hover {
- color: darken($lighter-text-color, 4%);
+ color: lighten($darker-text-color, 4%);
}
}
@@ -60,7 +59,7 @@
color: $highlight-text-color;
&:hover {
- color: darken($highlight-text-color, 4%);
+ color: lighten($highlight-text-color, 4%);
}
.emoji-mart-anchor-bar {
@@ -95,7 +94,7 @@
height: 270px;
max-height: 35vh;
padding: 0 6px 6px;
- background: $simple-background-color;
+ background: var(--dropdown-background-color);
will-change: transform;
&::-webkit-scrollbar-track:hover,
@@ -107,7 +106,7 @@
.emoji-mart-search {
padding: 10px;
padding-inline-end: 45px;
- background: $simple-background-color;
+ background: var(--dropdown-background-color);
position: relative;
input {
@@ -118,9 +117,9 @@
font-family: inherit;
display: block;
width: 100%;
- background: rgba($ui-secondary-color, 0.3);
- color: $inverted-text-color;
- border: 1px solid $ui-secondary-color;
+ background: $ui-base-color;
+ color: $darker-text-color;
+ border: 1px solid lighten($ui-base-color, 8%);
border-radius: 4px;
&::-moz-focus-inner {
@@ -155,11 +154,10 @@
&:disabled {
cursor: default;
pointer-events: none;
- opacity: 0.3;
}
svg {
- fill: $action-button-color;
+ fill: $darker-text-color;
}
}
@@ -185,7 +183,7 @@
inset-inline-start: 0;
width: 100%;
height: 100%;
- background-color: rgba($ui-secondary-color, 0.7);
+ background-color: var(--dropdown-border-color);
border-radius: 100%;
}
}
@@ -202,7 +200,7 @@
width: 100%;
font-weight: 500;
padding: 5px 6px;
- background: $simple-background-color;
+ background: var(--dropdown-background-color);
}
}
@@ -246,7 +244,7 @@
.emoji-mart-no-results {
font-size: 14px;
- color: $light-text-color;
+ color: $dark-text-color;
text-align: center;
padding: 5px 6px;
padding-top: 70px;
diff --git a/app/javascript/styles/mastodon/modal.scss b/app/javascript/styles/mastodon/modal.scss
index 0b7220b21d..60e7d62245 100644
--- a/app/javascript/styles/mastodon/modal.scss
+++ b/app/javascript/styles/mastodon/modal.scss
@@ -1,5 +1,5 @@
.modal-layout {
- background: $ui-base-color
+ background: darken($ui-base-color, 4%)
url('data:image/svg+xml;utf8,
')
repeat-x bottom fixed;
display: flex;
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index 8a26e611ca..939fca3364 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -52,6 +52,8 @@
&__option {
position: relative;
display: flex;
+ align-items: flex-start;
+ gap: 8px;
padding: 6px 0;
line-height: 18px;
cursor: default;
@@ -78,16 +80,22 @@
box-sizing: border-box;
width: 100%;
font-size: 14px;
- color: $inverted-text-color;
+ color: $secondary-text-color;
outline: 0;
font-family: inherit;
- background: $simple-background-color;
- border: 1px solid darken($simple-background-color, 14%);
+ background: $ui-base-color;
+ border: 1px solid $darker-text-color;
border-radius: 4px;
- padding: 6px 10px;
+ padding: 8px 12px;
&:focus {
- border-color: $highlight-text-color;
+ border-color: $ui-highlight-color;
+ }
+
+ @media screen and (width <= 600px) {
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0.5px;
}
}
@@ -96,26 +104,20 @@
}
&.editable {
- display: flex;
align-items: center;
overflow: visible;
}
}
&__input {
- display: inline-block;
+ display: block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
- width: 18px;
- height: 18px;
- margin-inline-end: 10px;
- top: -1px;
+ width: 17px;
+ height: 17px;
border-radius: 50%;
- vertical-align: middle;
- margin-top: auto;
- margin-bottom: auto;
- flex: 0 0 18px;
+ flex: 0 0 auto;
&.checkbox {
border-radius: 4px;
@@ -159,6 +161,15 @@
}
}
+ &__option.editable &__input {
+ &:active,
+ &:focus,
+ &:hover {
+ border-color: $ui-primary-color;
+ border-width: 1px;
+ }
+ }
+
&__number {
display: inline-block;
width: 45px;
@@ -209,90 +220,6 @@
}
}
-.compose-form__poll-wrapper {
- border-top: 1px solid darken($simple-background-color, 8%);
-
- ul {
- padding: 10px;
- }
-
- .poll__input {
- &:active,
- &:focus,
- &:hover {
- border-color: $ui-button-focus-background-color;
- }
- }
-
- .poll__footer {
- border-top: 1px solid darken($simple-background-color, 8%);
- padding: 10px;
- display: flex;
- align-items: center;
-
- button,
- select {
- flex: 1 1 50%;
-
- &:focus {
- border-color: $highlight-text-color;
- }
- }
- }
-
- .button.button-secondary {
- font-size: 14px;
- font-weight: 400;
- padding: 6px 10px;
- height: auto;
- line-height: inherit;
- color: $action-button-color;
- border-color: $action-button-color;
- margin-inline-end: 5px;
-
- &:hover,
- &:focus,
- &.active {
- border-color: $action-button-color;
- background-color: $action-button-color;
- color: $ui-button-color;
- }
- }
-
- li {
- display: flex;
- align-items: center;
-
- .poll__option {
- flex: 0 0 auto;
- width: calc(100% - (23px + 6px));
- margin-inline-end: 6px;
- }
- }
-
- select {
- appearance: none;
- box-sizing: border-box;
- font-size: 14px;
- color: $inverted-text-color;
- display: inline-block;
- width: auto;
- outline: 0;
- font-family: inherit;
- background: $simple-background-color
- url("data:image/svg+xml;utf8,
")
- no-repeat right 8px center / auto 16px;
- border: 1px solid darken($simple-background-color, 14%);
- border-radius: 4px;
- padding: 6px 10px;
- padding-inline-end: 30px;
- }
-
- .icon-button.disabled {
- color: darken($simple-background-color, 14%);
- }
-}
-
.muted .poll {
color: $dark-text-color;
diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb
index 059860979c..1d5b2f48c6 100644
--- a/app/lib/activitypub/parser/status_parser.rb
+++ b/app/lib/activitypub/parser/status_parser.rb
@@ -4,14 +4,15 @@ class ActivityPub::Parser::StatusParser
include JsonLdHelper
# @param [Hash] json
- # @param [Hash] magic_values
- # @option magic_values [String] :followers_collection
- def initialize(json, magic_values = {})
- @json = json
- @object = magic_values[:object] || json['object'] || json
- @magic_values = magic_values
- @account = magic_values[:account]
- @friend = magic_values[:friend_domain]
+ # @param [Hash] options
+ # @option options [String] :followers_collection
+ # @option options [Hash] :object
+ def initialize(json, **options)
+ @json = json
+ @object = options[:object] || json['object'] || json
+ @options = options
+ @account = options[:account]
+ @friend = options[:friend_domain]
end
def uri
@@ -84,7 +85,7 @@ class ActivityPub::Parser::StatusParser
:unlisted
elsif audience_to.include?('kmyblue:LoginOnly') || audience_to.include?('as:LoginOnly') || audience_to.include?('LoginUser')
:login
- elsif audience_to.include?(@magic_values[:followers_collection])
+ elsif audience_to.include?(@options[:followers_collection])
:private
else
:direct
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
index 71f5600d3d..865f7bdbbc 100644
--- a/app/models/custom_filter.rb
+++ b/app/models/custom_filter.rb
@@ -71,16 +71,7 @@ class CustomFilter < ApplicationRecord
scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
scope.to_a.group_by(&:custom_filter).each do |filter, keywords|
- keywords.map! do |keyword|
- if keyword.whole_word
- sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
- eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : ''
-
- /(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/
- else
- /#{Regexp.escape(keyword.keyword)}/i
- end
- end
+ keywords.map!(&:to_regex)
filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter }
end.to_h
diff --git a/app/models/custom_filter_keyword.rb b/app/models/custom_filter_keyword.rb
index 3a853daf30..7cc9b6fef9 100644
--- a/app/models/custom_filter_keyword.rb
+++ b/app/models/custom_filter_keyword.rb
@@ -23,8 +23,24 @@ class CustomFilterKeyword < ApplicationRecord
before_destroy :prepare_cache_invalidation!
after_commit :invalidate_cache!
+ def to_regex
+ if whole_word?
+ /(?mix:#{to_regex_sb}#{Regexp.escape(keyword)}#{to_regex_eb})/
+ else
+ /#{Regexp.escape(keyword)}/i
+ end
+ end
+
private
+ def to_regex_sb
+ /\A[[:word:]]/.match?(keyword) ? '\b' : ''
+ end
+
+ def to_regex_eb
+ /[[:word:]]\z/.match?(keyword) ? '\b' : ''
+ end
+
def prepare_cache_invalidation!
custom_filter.prepare_cache_invalidation!
end
diff --git a/app/models/instance.rb b/app/models/instance.rb
index ac6c250d47..655f0183c7 100644
--- a/app/models/instance.rb
+++ b/app/models/instance.rb
@@ -27,11 +27,25 @@ class Instance < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) }
scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
+ scope :with_domain_follows, ->(domains) { where(domain: domains).where(domain_account_follows) }
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end
+ def self.domain_account_follows
+ Arel.sql(
+ <<~SQL.squish
+ EXISTS (
+ SELECT 1
+ FROM follows
+ JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id
+ WHERE accounts.domain = instances.domain
+ )
+ SQL
+ )
+ end
+
def readonly?
true
end
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 2c0d3aa56f..f846a9bc75 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -94,7 +94,7 @@ class StatusPolicy < ApplicationPolicy
if record.mentions.loaded?
record.mentions.any? { |mention| mention.account_id == current_account.id }
else
- record.mentions.where(account: current_account).exists?
+ record.mentions.exists?(account: current_account)
end
end
diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb
index 23b2fa514b..8cee271272 100644
--- a/app/serializers/rest/announcement_serializer.rb
+++ b/app/serializers/rest/announcement_serializer.rb
@@ -23,7 +23,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
end
def read
- object.announcement_mutes.where(account: current_user.account).exists?
+ object.announcement_mutes.exists?(account: current_user.account)
end
def content
diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb
index 73ae268bee..a18f38556b 100644
--- a/app/workers/move_worker.rb
+++ b/app/workers/move_worker.rb
@@ -123,7 +123,7 @@ class MoveWorker
end
def add_account_note_if_needed!(account, id)
- unless AccountNote.where(account: account, target_account: @target_account).exists?
+ unless AccountNote.exists?(account: account, target_account: @target_account)
text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do
I18n.t(id, acct: @source_account.acct)
end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3c13ada380..a855f5a16b 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -85,7 +85,7 @@ Rails.application.configure do
# If using a Heroku, Vagrant or generic remote development environment,
# use letter_opener_web, accessible at /letter_opener.
# Otherwise, use letter_opener, which launches a browser window to view sent mail.
- config.action_mailer.delivery_method = (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) ? :letter_opener_web : :letter_opener
+ config.action_mailer.delivery_method = ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV'] ? :letter_opener_web : :letter_opener
# We provide a default secret for the development environment here.
# This value should not be used in production environments!
diff --git a/config/locales/br.yml b/config/locales/br.yml
index 7af72457d0..d20609a8ce 100644
--- a/config/locales/br.yml
+++ b/config/locales/br.yml
@@ -443,6 +443,9 @@ br:
preferences:
other: All
posting_defaults: Arventennoù embann dre ziouer
+ redirects:
+ prompt: M'ho peus fiziañs el liamm-mañ, klikit warnañ evit kenderc'hel.
+ title: O kuitaat %{instance} emaoc'h.
relationships:
dormant: O kousket
followers: Heulier·ezed·ien
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 38ef976b83..58f6e26374 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -1546,6 +1546,9 @@ ca:
errors:
limit_reached: Límit de diferents reaccions assolit
unrecognized_emoji: no és un emoji reconegut
+ redirects:
+ prompt: Si confieu en aquest enllaç, feu-hi clic per a continuar.
+ title: Esteu sortint de %{instance}.
relationships:
activity: Activitat del compte
confirm_follow_selected_followers: Segur que vols seguir els seguidors seleccionats?
diff --git a/config/locales/da.yml b/config/locales/da.yml
index d92d001905..57899d5f71 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -1546,6 +1546,9 @@ da:
errors:
limit_reached: Grænse for forskellige reaktioner nået
unrecognized_emoji: er ikke en genkendt emoji
+ redirects:
+ prompt: Er der tillid til dette link, så klik på det for at fortsætte.
+ title: Nu forlades %{instance}.
relationships:
activity: Kontoaktivitet
confirm_follow_selected_followers: Sikker på, at de valgte følgere skal følges?
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 9568f698d1..b77f415190 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1546,6 +1546,9 @@ de:
errors:
limit_reached: Limit für verschiedene Reaktionen erreicht
unrecognized_emoji: ist ein unbekanntes Emoji
+ redirects:
+ prompt: Wenn du diesem Link vertraust, dann klicke ihn an, um fortzufahren.
+ title: Du verlässt %{instance}.
relationships:
activity: Kontoaktivität
confirm_follow_selected_followers: Möchtest du den ausgewählten Followern folgen?
diff --git a/config/locales/devise.fr-CA.yml b/config/locales/devise.fr-CA.yml
index 34104e0ac5..7f13f67828 100644
--- a/config/locales/devise.fr-CA.yml
+++ b/config/locales/devise.fr-CA.yml
@@ -73,9 +73,13 @@ fr-CA:
subject: 'Mastodon: Clé de sécurité supprimée'
title: Une de vos clés de sécurité a été supprimée
webauthn_disabled:
+ explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte.
+ extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée.
subject: 'Mastodon: Authentification avec clés de sécurité désactivée'
title: Clés de sécurité désactivées
webauthn_enabled:
+ explanation: L'authentification par clé de sécurité a été activée pour votre compte.
+ extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter.
subject: 'Mastodon: Authentification de la clé de sécurité activée'
title: Clés de sécurité activées
omniauth_callbacks:
diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml
index 1fc6663bfe..8a5b8384e0 100644
--- a/config/locales/devise.fr.yml
+++ b/config/locales/devise.fr.yml
@@ -73,9 +73,13 @@ fr:
subject: 'Mastodon: Clé de sécurité supprimée'
title: Une de vos clés de sécurité a été supprimée
webauthn_disabled:
+ explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte.
+ extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée.
subject: 'Mastodon: Authentification avec clés de sécurité désactivée'
title: Clés de sécurité désactivées
webauthn_enabled:
+ explanation: L'authentification par clé de sécurité a été activée pour votre compte.
+ extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter.
subject: 'Mastodon: Authentification de la clé de sécurité activée'
title: Clés de sécurité activées
omniauth_callbacks:
diff --git a/config/locales/devise.sv.yml b/config/locales/devise.sv.yml
index b089f21427..6544f426bd 100644
--- a/config/locales/devise.sv.yml
+++ b/config/locales/devise.sv.yml
@@ -77,6 +77,7 @@ sv:
subject: 'Mastodon: Autentisering med säkerhetsnycklar är inaktiverat'
title: Säkerhetsnycklar inaktiverade
webauthn_enabled:
+ extra: Din säkerhetsnyckel kan nu användas för inloggning.
subject: 'Mastodon: Autentisering med säkerhetsnyckel är aktiverat'
title: Säkerhetsnycklar aktiverade
omniauth_callbacks:
diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml
index 762c8eba84..06438971a7 100644
--- a/config/locales/devise.zh-TW.yml
+++ b/config/locales/devise.zh-TW.yml
@@ -47,14 +47,14 @@ zh-TW:
subject: Mastodon:重設密碼指引
title: 重設密碼
two_factor_disabled:
- explanation: 現在僅可使用電子郵件地址與密碼登入。
+ explanation: 目前僅可使用電子郵件地址與密碼登入。
subject: Mastodon:已停用兩階段驗證
- subtitle: 您帳號的兩步驟驗證已停用。
+ subtitle: 您帳號之兩階段驗證已停用。
title: 已停用兩階段驗證
two_factor_enabled:
- explanation: 登入時需要配對的 TOTP 應用程式產生的權杖。
+ explanation: 登入時需要配對的 TOTP 應用程式產生之 token。
subject: Mastodon:已啟用兩階段驗證
- subtitle: 您的帳號已啟用兩步驟驗證。
+ subtitle: 您的帳號之兩階段驗證已啟用。
title: 已啟用兩階段驗證
two_factor_recovery_codes_changed:
explanation: 之前的備用驗證碼已經失效,且已產生新的。
@@ -74,12 +74,12 @@ zh-TW:
title: 您的一支安全密鑰已經被移除
webauthn_disabled:
explanation: 您的帳號已停用安全金鑰身份驗證。
- extra: 現在僅可使用配對的 TOTP 應用程式產生的權杖登入。
+ extra: 現在僅可使用配對的 TOTP 應用程式產生之 token 登入。
subject: Mastodon:安全密鑰認證方式已停用
title: 已停用安全密鑰
webauthn_enabled:
- explanation: 您的帳號已啟用安全金鑰驗證。
- extra: 您的安全金鑰現在可用於登入。
+ explanation: 您的帳號已啟用安全金鑰身分驗證。
+ extra: 您的安全金鑰現在已可用於登入。
subject: Mastodon:已啟用安全密鑰認證
title: 已啟用安全密鑰
omniauth_callbacks:
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index cc55d3d3ff..d1dbdbf0b8 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -1546,6 +1546,9 @@ es-AR:
errors:
limit_reached: Se alcanzó el límite de reacciones diferentes
unrecognized_emoji: no es un emoji conocido
+ redirects:
+ prompt: Si confiás en este enlace, dale clic o un toque para continuar.
+ title: Estás dejando %{instance}.
relationships:
activity: Actividad de la cuenta
confirm_follow_selected_followers: "¿Estás seguro que querés seguir a los seguidores seleccionados?"
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index b84fb7cf96..4d228e98d4 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -1546,6 +1546,9 @@ es-MX:
errors:
limit_reached: Límite de reacciones diferentes alcanzado
unrecognized_emoji: no es un emoji conocido
+ redirects:
+ prompt: Si confías en este enlace, púlsalo para continuar.
+ title: Vas a salir de %{instance}.
relationships:
activity: Actividad de la cuenta
confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 95816d6bcb..08fc0988e4 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1546,6 +1546,9 @@ es:
errors:
limit_reached: Límite de reacciones diferentes alcanzado
unrecognized_emoji: no es un emoji conocido
+ redirects:
+ prompt: Si confías en este enlace, púlsalo para continuar.
+ title: Vas a salir de %{instance}.
relationships:
activity: Actividad de la cuenta
confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?"
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index bd6ea8c832..44688577a9 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -1550,6 +1550,9 @@ eu:
errors:
limit_reached: Erreakzio desberdinen muga gaindituta
unrecognized_emoji: ez da emoji ezaguna
+ redirects:
+ prompt: Esteka honetan fidatzen bazara, egin klik jarraitzeko.
+ title: "%{instance} instantziatik zoaz."
relationships:
activity: Kontuaren aktibitatea
confirm_follow_selected_followers: Ziur hautatutako jarraitzaileei jarraitu nahi dituzula?
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 8e61c7b2a0..856532f8f1 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -1546,6 +1546,9 @@ fi:
errors:
limit_reached: Erilaisten reaktioiden raja saavutettu
unrecognized_emoji: ei ole tunnistettu emoji
+ redirects:
+ prompt: Jos luotat tähän linkkiin, jatka napsauttamalla.
+ title: Olet poistumassa palvelimelta %{instance}.
relationships:
activity: Tilin aktiivisuus
confirm_follow_selected_followers: Haluatko varmasti seurata valittuja seuraajia?
@@ -1791,8 +1794,8 @@ fi:
subject: Arkisto on valmiina ladattavaksi
title: Arkiston tallennus
failed_2fa:
- details: 'Tässä on tiedot kirjautumisyrityksestä:'
- explanation: Joku on yrittänyt kirjautua tilillesi, mutta antanut virheellisen kaksivaiheisen todennuksen.
+ details: 'Tässä on tietoja kirjautumisyrityksestä:'
+ explanation: Joku on yrittänyt kirjautua tilillesi mutta on antanut virheellisen toisen vaiheen todennustekijän.
further_actions_html: Jos se et ollut sinä, suosittelemme, että %{action} välittömästi, sillä se on saattanut vaarantua.
subject: Kaksivaiheisen todennuksen virhe
title: Epäonnistunut kaksivaiheinen todennus
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 8e34265313..10b1e76f5f 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -1546,6 +1546,9 @@ fo:
errors:
limit_reached: Mark fyri ymisk aftursvar rokkið
unrecognized_emoji: er ikki eitt kenslutekn, sum kennist aftur
+ redirects:
+ prompt: Um tú lítir á hetta leinkið, so kanst tú klikkja á tað fyri at halda fram.
+ title: Tú fer burtur úr %{instance}.
relationships:
activity: Kontuvirksemi
confirm_follow_selected_followers: Vil tú veruliga fylgja valdu fylgjarunum?
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index dbdff5f52c..3676d0b7b5 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -1546,6 +1546,9 @@ fr-CA:
errors:
limit_reached: Limite de réactions différentes atteinte
unrecognized_emoji: n’est pas un émoji reconnu
+ redirects:
+ prompt: Si vous faites confiance à ce lien, cliquez pour continuer.
+ title: Vous quittez %{instance}.
relationships:
activity: Activité du compte
confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ?
@@ -1790,6 +1793,12 @@ fr-CA:
extra: Elle est maintenant prête à être téléchargée !
subject: Votre archive est prête à être téléchargée
title: Récupération de l’archive
+ failed_2fa:
+ details: 'Voici les détails de la tentative de connexion :'
+ explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide.
+ further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis.
+ subject: Échec de l'authentification à double facteur
+ title: Échec de l'authentification à double facteur
suspicious_sign_in:
change_password: changer votre mot de passe
details: 'Voici les détails de la connexion :'
@@ -1843,6 +1852,7 @@ fr-CA:
go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité
invalid_otp_token: Le code d’authentification à deux facteurs est invalide
otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email}
+ rate_limited: Trop de tentatives d'authentification, réessayez plus tard.
seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles.
signed_in_as: 'Connecté·e en tant que :'
verification:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index fe1a219a31..a3aaf7a26e 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1546,6 +1546,9 @@ fr:
errors:
limit_reached: Limite de réactions différentes atteinte
unrecognized_emoji: n’est pas un émoji reconnu
+ redirects:
+ prompt: Si vous faites confiance à ce lien, cliquez pour continuer.
+ title: Vous quittez %{instance}.
relationships:
activity: Activité du compte
confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ?
@@ -1790,6 +1793,12 @@ fr:
extra: Elle est maintenant prête à être téléchargée !
subject: Votre archive est prête à être téléchargée
title: Récupération de l’archive
+ failed_2fa:
+ details: 'Voici les détails de la tentative de connexion :'
+ explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide.
+ further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis.
+ subject: Échec de l'authentification à double facteur
+ title: Échec de l'authentification à double facteur
suspicious_sign_in:
change_password: changer votre mot de passe
details: 'Voici les détails de la connexion :'
@@ -1843,6 +1852,7 @@ fr:
go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité
invalid_otp_token: Le code d’authentification à deux facteurs est invalide
otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email}
+ rate_limited: Trop de tentatives d'authentification, réessayez plus tard.
seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles.
signed_in_as: 'Connecté·e en tant que :'
verification:
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 087ed2ec76..7b3fd1a6eb 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -1546,6 +1546,9 @@ gl:
errors:
limit_reached: Acadouse o límite das diferentes reaccións
unrecognized_emoji: non é unha emoticona recoñecida
+ redirects:
+ prompt: Se confías nesta ligazón, preme nela para continuar.
+ title: Vas saír de %{instance}.
relationships:
activity: Actividade da conta
confirm_follow_selected_followers: Tes a certeza de querer seguir as seguidoras seleccionadas?
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 1f5fd096ac..05b52213a7 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -1598,6 +1598,9 @@ he:
errors:
limit_reached: גבול מספר התגובות השונות הושג
unrecognized_emoji: הוא לא אמוג'י מוכר
+ redirects:
+ prompt: יש ללחוץ על הקישור, אם לדעתך ניתן לסמוך עליו.
+ title: יציאה מתוך %{instance}.
relationships:
activity: רמת פעילות
confirm_follow_selected_followers: האם את/ה בטוח/ה שברצונך לעקוב אחרי החשבונות שסומנו?
@@ -1856,7 +1859,7 @@ he:
title: הוצאת ארכיון
failed_2fa:
details: 'הנה פרטי נסיון ההתחברות:'
- explanation: פולני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל.
+ explanation: פלוני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל.
further_actions_html: אם הנסיון לא היה שלך, אנו ממליצים על %{action} באופן מיידי כדי שהחשבון לא יפול קורבן.
subject: נכשל אימות בגורם שני
title: אימות בגורם שני נכשל
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 2870435ea7..8cbbb64c97 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -1546,6 +1546,8 @@ hu:
errors:
limit_reached: A különböző reakciók száma elérte a határértéket
unrecognized_emoji: nem ismert emodzsi
+ redirects:
+ prompt: Ha megbízunk ebben a hivatkozásban, kattintsunk rá a folytatáshoz.
relationships:
activity: Fiók aktivitás
confirm_follow_selected_followers: Biztos, hogy követni akarod a kiválasztott követőket?
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 191383f56c..d374c60755 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -1550,6 +1550,9 @@ is:
errors:
limit_reached: Hámarki mismunandi viðbragða náð
unrecognized_emoji: er ekki þekkt tjáningartákn
+ redirects:
+ prompt: Ef þú treystir þessum tengli, geturðu smellt á hann til að halda áfram.
+ title: Þú ert að yfirgefa %{instance}.
relationships:
activity: Virkni aðgangs
confirm_follow_selected_followers: Ertu viss um að þú viljir fylgjast með völdum fylgjendum?
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 89ff071f36..31de2252d1 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -1548,6 +1548,9 @@ it:
errors:
limit_reached: Raggiunto il limite di reazioni diverse
unrecognized_emoji: non è un emoji riconosciuto
+ redirects:
+ prompt: Se ti fidi di questo collegamento, fai clic su di esso per continuare.
+ title: Stai lasciando %{instance}.
relationships:
activity: Attività dell'account
confirm_follow_selected_followers: Sei sicuro di voler seguire i follower selezionati?
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 39559279e0..401a7ffb06 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -2005,6 +2005,12 @@ ja:
extra: ダウンロードの準備ができました!
subject: アーカイブの準備ができました
title: アーカイブの取り出し
+ failed_2fa:
+ details: '試行されたログインの詳細は以下のとおりです:'
+ explanation: アカウントへのログインが試行されましたが、二要素認証で不正な回答が送信されました。
+ further_actions_html: このログインに心当たりがない場合は、ただちに%{action}してください。
+ subject: 二要素認証に失敗しました
+ title: 二要素認証に失敗した記録があります
suspicious_sign_in:
change_password: パスワードを変更
details: 'ログインの詳細は以下のとおりです:'
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index b3c786e265..9f4f1343c7 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -1522,6 +1522,9 @@ ko:
errors:
limit_reached: 리액션 갯수 제한에 도달했습니다
unrecognized_emoji: 인식 되지 않은 에모지입니다
+ redirects:
+ prompt: 이 링크를 믿을 수 있다면, 클릭해서 계속하세요.
+ title: "%{instance}를 떠나려고 합니다."
relationships:
activity: 계정 활동
confirm_follow_selected_followers: 정말로 선택된 팔로워들을 팔로우하시겠습니까?
@@ -1762,6 +1765,10 @@ ko:
title: 아카이브 테이크아웃
failed_2fa:
details: '로그인 시도에 대한 상세 정보입니다:'
+ explanation: 누군가가 내 계정에 로그인을 시도했지만 2차인증에 올바른 값을 입력하지 못했습니다.
+ further_actions_html: 만약 당신이 한 게 아니었다면 유출의 가능성이 있으니 가능한 빨리 %{action} 하시기 바랍니다.
+ subject: 2차 인증 실패
+ title: 2차 인증에 실패했습니다
suspicious_sign_in:
change_password: 암호 변경
details: '로그인에 대한 상세 정보입니다:'
diff --git a/config/locales/lad.yml b/config/locales/lad.yml
index be5d2d21bd..02308cf2f0 100644
--- a/config/locales/lad.yml
+++ b/config/locales/lad.yml
@@ -1516,6 +1516,9 @@ lad:
errors:
limit_reached: Limito de reaksyones desferentes alkansado
unrecognized_emoji: no es un emoji konesido
+ redirects:
+ prompt: Si konfiyas en este atadijo, klikalo para kontinuar.
+ title: Estas salyendo de %{instance}.
relationships:
activity: Aktivita del kuento
confirm_follow_selected_followers: Estas siguro ke keres segir a los suivantes eskojidos?
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index ba8b53fdc9..1d159bf45a 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -478,6 +478,9 @@ lt:
other: Kita
privacy:
hint_html: "
Tikrink, kaip nori, kad tavo profilis ir įrašai būtų randami. Įjungus įvairias Mastodon funkcijas, jos gali padėti pasiekti platesnę auditoriją. Akimirką peržiūrėk šiuos nustatymus, kad įsitikintum, jog jie atitinka tavo naudojimo būdą."
+ redirects:
+ prompt: Jei pasitiki šia nuoroda, spustelėk ją, kad tęstum.
+ title: Palieki %{instance}
remote_follow:
missing_resource: Jūsų paskyros nukreipimo URL nerasta
scheduled_statuses:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 2d27f9165d..a3657890da 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1546,6 +1546,9 @@ nl:
errors:
limit_reached: Limiet van verschillende emoji-reacties bereikt
unrecognized_emoji: is geen bestaande emoji-reactie
+ redirects:
+ prompt: Als je deze link vertrouwt, klik er dan op om door te gaan.
+ title: Je verlaat %{instance}.
relationships:
activity: Accountactiviteit
confirm_follow_selected_followers: Weet je zeker dat je de geselecteerde volgers wilt volgen?
@@ -1792,7 +1795,8 @@ nl:
title: Archief ophalen
failed_2fa:
details: 'Hier zijn details van de aanmeldpoging:'
- explanation: Iemand heeft geprobeerd om in te loggen op uw account maar heeft een ongeldige tweede verificatiefactor opgegeven.
+ explanation: Iemand heeft geprobeerd om in te loggen op jouw account maar heeft een ongeldige tweede verificatiefactor opgegeven.
+ further_actions_html: Als jij dit niet was, raden we je aan om onmiddellijk %{action} aangezien het in gevaar kan zijn.
subject: Tweede factor authenticatiefout
title: Tweestapsverificatie mislukt
suspicious_sign_in:
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 95eed49785..ffa5198a3a 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -1546,6 +1546,9 @@ nn:
errors:
limit_reached: Grensen for forskjellige reaksjoner nådd
unrecognized_emoji: er ikke en gjenkjent emoji
+ redirects:
+ prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette.
+ title: Du forlater %{instance}.
relationships:
activity: Kontoaktivitet
confirm_follow_selected_followers: Er du sikker på at du ynskjer å fylgja dei valde fylgjarane?
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 7ece8564fc..d26b20379e 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -1546,6 +1546,9 @@
errors:
limit_reached: Grensen for ulike reaksjoner nådd
unrecognized_emoji: er ikke en gjenkjent emoji
+ redirects:
+ prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette.
+ title: Du forlater %{instance}.
relationships:
activity: Kontoaktivitet
confirm_follow_selected_followers: Er du sikker på at du vil følge valgte følgere?
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 6718f1994b..5bc78a6adf 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -1598,6 +1598,9 @@ pl:
errors:
limit_reached: Przekroczono limit różnych reakcji
unrecognized_emoji: nie jest znanym emoji
+ redirects:
+ prompt: Kliknij ten link jeżeli mu ufasz.
+ title: Opuszczasz %{instance}.
relationships:
activity: Aktywność konta
confirm_follow_selected_followers: Czy na pewno chcesz obserwować wybranych obserwujących?
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index c1a47c0161..79396d627f 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1546,6 +1546,9 @@ pt-BR:
errors:
limit_reached: Limite de reações diferentes atingido
unrecognized_emoji: não é um emoji reconhecido
+ redirects:
+ prompt: Se você confia neste link, clique nele para continuar.
+ title: Você está saindo de %{instance}.
relationships:
activity: Atividade da conta
confirm_follow_selected_followers: Tem certeza que deseja seguir os seguidores selecionados?
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 268531718d..8a20bc68a1 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -1546,6 +1546,9 @@ pt-PT:
errors:
limit_reached: Alcançado limite de reações diferentes
unrecognized_emoji: não é um emoji reconhecido
+ redirects:
+ prompt: Se confia nesta hiperligação, clique nela para continuar.
+ title: Está a deixar %{instance}.
relationships:
activity: Atividade da conta
confirm_follow_selected_followers: Tem a certeza que deseja seguir os seguidores selecionados?
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 24edbdc75e..04e49e0427 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -1598,6 +1598,9 @@ ru:
errors:
limit_reached: Достигнут лимит разных реакций
unrecognized_emoji: не является распознанным эмодзи
+ redirects:
+ prompt: Если вы доверяете этой ссылке, нажмите на нее, чтобы продолжить.
+ title: Вы покидаете %{instance}.
relationships:
activity: Активность учётной записи
confirm_follow_selected_followers: Вы уверены, что хотите подписаться на выбранных подписчиков?
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index e83ae348f6..20df763463 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -1101,6 +1101,9 @@ sk:
errors:
limit_reached: Maximálny počet rôznorodých reakcií bol dosiahnutý
unrecognized_emoji: je neznámy smajlík
+ redirects:
+ prompt: Ak tomuto odkazu veríš, klikni naňho pre pokračovanie.
+ title: Opúšťaš %{instance}.
relationships:
activity: Aktivita účtu
confirm_follow_selected_followers: Si si istý/á, že chceš nasledovať vybraných sledujúcich?
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index d6e6925c70..3dd4731209 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -1542,6 +1542,9 @@ sq:
errors:
limit_reached: U mbërrit në kufirin e reagimeve të ndryshme
unrecognized_emoji: s’është emotikon i pranuar
+ redirects:
+ prompt: Nëse e besoni këtë lidhje, klikoni që të vazhdohet.
+ title: Po e braktisni %{instance}.
relationships:
activity: Veprimtari llogarie
confirm_follow_selected_followers: Jeni i sigurt se doni të ndiqet ndjekësit e përzgjedhur?
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 9cb555c943..b55b6e0d19 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -1572,6 +1572,9 @@ sr-Latn:
errors:
limit_reached: Dostignuto je ograničenje različitih reakcija
unrecognized_emoji: nije prepoznat emodži
+ redirects:
+ prompt: Ako verujete ovoj vezi, kliknite na nju za nastavak.
+ title: Napuštate %{instance}.
relationships:
activity: Aktivnost naloga
confirm_follow_selected_followers: Da li ste sigurni da želite da pratite izabrane pratioce?
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index e1c2e992ed..8de7c90e73 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -1572,6 +1572,9 @@ sr:
errors:
limit_reached: Достигнуто је ограничење различитих реакција
unrecognized_emoji: није препознат емоџи
+ redirects:
+ prompt: Ако верујете овој вези, кликните на њу за наставак.
+ title: Напуштате %{instance}.
relationships:
activity: Активност налога
confirm_follow_selected_followers: Да ли сте сигурни да желите да пратите изабране пратиоце?
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index c9000d50fc..deac7cc638 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -1545,6 +1545,9 @@ sv:
errors:
limit_reached: Gränsen för unika reaktioner uppnådd
unrecognized_emoji: är inte en igenkänd emoji
+ redirects:
+ prompt: Om du litar på denna länk, klicka på den för att fortsätta.
+ title: Du lämnar %{instance}.
relationships:
activity: Kontoaktivitet
confirm_follow_selected_followers: Är du säker på att du vill följa valda följare?
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 2b5b5ad45b..b3a52715b7 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -1546,6 +1546,9 @@ tr:
errors:
limit_reached: Farklı reaksiyonların sınırına ulaşıldı
unrecognized_emoji: tanınan bir emoji değil
+ redirects:
+ prompt: Eğer bu bağlantıya güveniyorsanız, tıklayıp devam edebilirsiniz.
+ title: "%{instance} sunucusundan ayrılıyorsunuz."
relationships:
activity: Hesap etkinliği
confirm_follow_selected_followers: Seçili takipçileri takip etmek istediğinizden emin misiniz?
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 40a858d72a..531bdb3d59 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -1598,6 +1598,9 @@ uk:
errors:
limit_reached: Досягнуто обмеження різних реакцій
unrecognized_emoji: не є розпізнаним емоджі
+ redirects:
+ prompt: Якщо ви довіряєте цьому посиланню, натисніть, щоб продовжити.
+ title: Ви покидаєте %{instance}.
relationships:
activity: Діяльність облікового запису
confirm_follow_selected_followers: Ви справді бажаєте підписатися на обраних підписників?
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 1ece72e154..045a000e38 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -1520,6 +1520,9 @@ vi:
errors:
limit_reached: Bạn không nên thao tác liên tục
unrecognized_emoji: không phải là emoji
+ redirects:
+ prompt: Nếu bạn tin tưởng, hãy nhấn tiếp tục.
+ title: Bạn đang thoát khỏi %{instance}.
relationships:
activity: Tương tác
confirm_follow_selected_followers: Bạn có chắc muốn theo dõi những người đã chọn?
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 272787ce25..d1255bfefe 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1520,6 +1520,9 @@ zh-CN:
errors:
limit_reached: 互动种类的限制
unrecognized_emoji: 不是一个可识别的表情
+ redirects:
+ prompt: 如果您信任此链接,请单击以继续跳转。
+ title: 您正在离开 %{instance} 。
relationships:
activity: 账号活动
confirm_follow_selected_followers: 您确定想要关注所选的关注者吗?
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 0c39aa8c0b..b010a75c04 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -1520,6 +1520,9 @@ zh-HK:
errors:
limit_reached: 已達到可以給予反應極限
unrecognized_emoji: 不能識別這個emoji
+ redirects:
+ prompt: 如果你信任此連結,點擊它繼續。
+ title: 你即將離開 %{instance}。
relationships:
activity: 帳戶活動
confirm_follow_selected_followers: 你確定要追蹤選取的追蹤者嗎?
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 8726ea72a4..72e63e47d3 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -57,7 +57,7 @@ zh-TW:
destroyed_msg: 即將刪除 %{username} 的資料
disable: 停用
disable_sign_in_token_auth: 停用電子郵件 token 驗證
- disable_two_factor_authentication: 停用兩階段認證
+ disable_two_factor_authentication: 停用兩階段驗證
disabled: 已停用
display_name: 暱稱
domain: 站點
@@ -195,7 +195,7 @@ zh-TW:
destroy_status: 刪除狀態
destroy_unavailable_domain: 刪除無法存取的網域
destroy_user_role: 移除角色
- disable_2fa_user: 停用兩階段認證
+ disable_2fa_user: 停用兩階段驗證
disable_custom_emoji: 停用自訂顏文字
disable_sign_in_token_auth_user: 停用使用者電子郵件 token 驗證
disable_user: 停用帳號
@@ -254,7 +254,7 @@ zh-TW:
destroy_status_html: "%{name} 已刪除 %{target} 的嘟文"
destroy_unavailable_domain_html: "%{name} 已恢復對網域 %{target} 的發送"
destroy_user_role_html: "%{name} 已刪除 %{target} 角色"
- disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段認證 (2FA) "
+ disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段驗證 (2FA) "
disable_custom_emoji_html: "%{name} 已停用自訂表情符號 %{target}"
disable_sign_in_token_auth_user_html: "%{name} 已停用 %{target} 之使用者電子郵件 token 驗證"
disable_user_html: "%{name} 將使用者 %{target} 設定為禁止登入"
@@ -418,7 +418,7 @@ zh-TW:
view: 顯示已封鎖網域
email_domain_blocks:
add_new: 加入新項目
- allow_registrations_with_approval: 經允許後可註冊
+ allow_registrations_with_approval: 經審核後可註冊
attempts_over_week:
other: 上週共有 %{count} 次註冊嘗試
created_msg: 已成功將電子郵件網域加入黑名單
@@ -505,7 +505,7 @@ zh-TW:
delivery_available: 可傳送
delivery_error_days: 遞送失敗天數
delivery_error_hint: 若 %{count} 日皆無法遞送 ,則會自動標記無法遞送。
- destroyed_msg: 來自 %{domain} 的資料現在正在佇列中等待刪除。
+ destroyed_msg: 來自 %{domain} 的資料目前正在佇列中等待刪除。
empty: 找不到網域
known_accounts:
other: "%{count} 個已知帳號"
@@ -759,7 +759,7 @@ zh-TW:
title: 註冊
registrations_mode:
modes:
- approved: 註冊需要核准
+ approved: 註冊需要審核
none: 沒有人可註冊
open: 任何人皆能註冊
security:
@@ -870,7 +870,7 @@ zh-TW:
links:
allow: 允許連結
allow_provider: 允許發行者
- description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索現在世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。
+ description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索目前世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。
disallow: 不允許連結
disallow_provider: 不允許發行者
no_link_selected: 因未選取任何連結,所以什麼事都沒發生
@@ -1062,7 +1062,7 @@ zh-TW:
cas: CAS
saml: SAML
register: 註冊
- registration_closed: "%{instance} 現在不開放新成員"
+ registration_closed: "%{instance} 目前不開放新成員"
resend_confirmation: 重新傳送確認連結
reset_password: 重設密碼
rules:
@@ -1522,6 +1522,9 @@ zh-TW:
errors:
limit_reached: 達到可回應之上限
unrecognized_emoji: 並非一個可識別的 emoji
+ redirects:
+ prompt: 若您信任此連結,請點擊以繼續。
+ title: 您將要離開 %{instance} 。
relationships:
activity: 帳號動態
confirm_follow_selected_followers: 您確定要跟隨選取的跟隨者嗎?
@@ -1627,7 +1630,7 @@ zh-TW:
relationships: 跟隨中與跟隨者
statuses_cleanup: 自動嘟文刪除
strikes: 管理警告
- two_factor_authentication: 兩階段認證
+ two_factor_authentication: 兩階段驗證
webauthn_authentication: 安全金鑰
statuses:
attached:
@@ -1733,11 +1736,11 @@ zh-TW:
disable: 停用兩階段驗證
disabled_success: 已成功啟用兩階段驗證
edit: 編輯
- enabled: 兩階段認證已啟用
- enabled_success: 已成功啟用兩階段認證
+ enabled: 兩階段驗證已啟用
+ enabled_success: 兩階段驗證已成功啟用
generate_recovery_codes: 產生備用驗證碼
lost_recovery_codes: 讓您能於遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。
- methods: 兩步驟方式
+ methods: 兩階段驗證
otp: 驗證應用程式
recovery_codes: 備份備用驗證碼
recovery_codes_regenerated: 成功產生新的備用驗證碼
@@ -1757,15 +1760,15 @@ zh-TW:
title: 申訴被駁回
backup_ready:
explanation: 您要求完整備份您的 Mastodon 帳號。
- extra: 準備好下載了!
+ extra: 準備好可供下載了!
subject: 您的備份檔已可供下載
title: 檔案匯出
failed_2fa:
details: 以下是該登入嘗試之詳細資訊:
- explanation: 有人嘗試登入您的帳號,但提供了無效的第二個驗證因子。
+ explanation: 有人嘗試登入您的帳號,但提供了無效的兩階段驗證。
further_actions_html: 若這並非您所為,我們建議您立刻 %{action},因為其可能已被入侵。
- subject: 第二因子驗證失敗
- title: 第二因子身份驗證失敗
+ subject: 兩階段驗證失敗
+ title: 兩階段驗證失敗
suspicious_sign_in:
change_password: 變更密碼
details: 以下是該登入之詳細資訊:
@@ -1817,9 +1820,9 @@ zh-TW:
users:
follow_limit_reached: 您無法跟隨多於 %{limit} 個人
go_to_sso_account_settings: 前往您的身分提供商 (identity provider) 之帳號設定
- invalid_otp_token: 兩階段認證碼不正確
+ invalid_otp_token: 兩階段驗證碼不正確
otp_lost_help_html: 如果您無法存取這兩者,您可以透過 %{email} 與我們聯繫
- rate_limited: 身份驗證嘗試太多次,請稍後再試。
+ rate_limited: 過多次身份驗證嘗試,請稍後再試。
seamless_external_login: 由於您是由外部系統登入,所以不能設定密碼與電子郵件。
signed_in_as: 目前登入的帳號:
verification:
diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb
index 73012812fd..a64206065d 100644
--- a/lib/mastodon/cli/maintenance.rb
+++ b/lib/mastodon/cli/maintenance.rb
@@ -72,6 +72,10 @@ module Mastodon::CLI
local? ? username : "#{username}@#{domain}"
end
+ def db_table_exists?(table)
+ ActiveRecord::Base.connection.table_exists?(table)
+ end
+
# This is a duplicate of the Account::Merging concern because we need it
# to be independent from code version.
def merge_with!(other_account)
@@ -88,12 +92,12 @@ module Mastodon::CLI
AccountModerationNote, AccountPin, AccountStat, ListAccount,
PollVote, Mention
]
- owned_classes << AccountDeletionRequest if ActiveRecord::Base.connection.table_exists?(:account_deletion_requests)
- owned_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes)
- owned_classes << FollowRecommendationSuppression if ActiveRecord::Base.connection.table_exists?(:follow_recommendation_suppressions)
- owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs)
- owned_classes << Appeal if ActiveRecord::Base.connection.table_exists?(:appeals)
- owned_classes << BulkImport if ActiveRecord::Base.connection.table_exists?(:bulk_imports)
+ owned_classes << AccountDeletionRequest if db_table_exists?(:account_deletion_requests)
+ owned_classes << AccountNote if db_table_exists?(:account_notes)
+ owned_classes << FollowRecommendationSuppression if db_table_exists?(:follow_recommendation_suppressions)
+ owned_classes << AccountIdentityProof if db_table_exists?(:account_identity_proofs)
+ owned_classes << Appeal if db_table_exists?(:appeals)
+ owned_classes << BulkImport if db_table_exists?(:bulk_imports)
owned_classes.each do |klass|
klass.where(account_id: other_account.id).find_each do |record|
@@ -104,7 +108,7 @@ module Mastodon::CLI
end
target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin]
- target_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes)
+ target_classes << AccountNote if db_table_exists?(:account_notes)
target_classes.each do |klass|
klass.where(target_account_id: other_account.id).find_each do |record|
@@ -114,13 +118,13 @@ module Mastodon::CLI
end
end
- if ActiveRecord::Base.connection.table_exists?(:canonical_email_blocks)
+ if db_table_exists?(:canonical_email_blocks)
CanonicalEmailBlock.where(reference_account_id: other_account.id).find_each do |record|
record.update_attribute(:reference_account_id, id)
end
end
- if ActiveRecord::Base.connection.table_exists?(:appeals)
+ if db_table_exists?(:appeals)
Appeal.where(account_warning_id: other_account.id).find_each do |record|
record.update_attribute(:account_warning_id, id)
end
@@ -234,16 +238,16 @@ module Mastodon::CLI
say 'Restoring index_accounts_on_username_and_domain_lower…'
if migrator_version < 2020_06_20_164023
- ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true
+ database_connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true
else
- ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
+ database_connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
end
say 'Reindexing textual indexes on accounts…'
- ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;')
- ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;')
- ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;')
- ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515
+ rebuild_index(:search_index)
+ rebuild_index(:index_accounts_on_uri)
+ rebuild_index(:index_accounts_on_url)
+ rebuild_index(:index_accounts_on_domain_and_id) if migrator_version >= 2023_05_24_190515
end
def deduplicate_users!
@@ -260,21 +264,21 @@ module Mastodon::CLI
deduplicate_users_process_password_token
say 'Restoring users indexes…'
- ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
- ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
- ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010
+ database_connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
+ database_connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
+ database_connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010
if migrator_version < 2022_03_10_060641
- ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
+ database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
else
- ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops
+ database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops
end
- ActiveRecord::Base.connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753
+ rebuild_index(:index_users_on_unconfirmed_email) if migrator_version >= 2023_07_02_151753
end
def deduplicate_users_process_email
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a
ref_user = users.shift
say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow
@@ -288,7 +292,7 @@ module Mastodon::CLI
end
def deduplicate_users_process_confirmation_token
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).order(created_at: :desc).includes(:account).to_a.drop(1)
say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
@@ -300,7 +304,7 @@ module Mastodon::CLI
def deduplicate_users_process_remember_token
if migrator_version < 2022_01_18_183010
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1)
say "Unsetting remember token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
@@ -312,7 +316,7 @@ module Mastodon::CLI
end
def deduplicate_users_process_password_token
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a.drop(1)
say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
@@ -326,47 +330,47 @@ module Mastodon::CLI
remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain')
say 'Removing duplicate account domain blocks…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row|
AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all
end
say 'Restoring account domain blocks indexes…'
- ActiveRecord::Base.connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true
+ database_connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true
end
def deduplicate_account_identity_proofs!
- return unless ActiveRecord::Base.connection.table_exists?(:account_identity_proofs)
+ return unless db_table_exists?(:account_identity_proofs)
remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username')
say 'Removing duplicate account identity proofs…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row|
AccountIdentityProof.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring account identity proofs indexes…'
- ActiveRecord::Base.connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true
+ database_connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true
end
def deduplicate_announcement_reactions!
- return unless ActiveRecord::Base.connection.table_exists?(:announcement_reactions)
+ return unless db_table_exists?(:announcement_reactions)
remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id')
say 'Removing duplicate announcement reactions…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row|
AnnouncementReaction.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring announcement_reactions indexes…'
- ActiveRecord::Base.connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true
+ database_connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true
end
def deduplicate_conversations!
remove_index_if_exists!(:conversations, 'index_conversations_on_uri')
say 'Deduplicating conversations…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
conversations = Conversation.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_conversation = conversations.shift
@@ -379,9 +383,9 @@ module Mastodon::CLI
say 'Restoring conversations indexes…'
if migrator_version < 2022_03_07_083603
- ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true
+ database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true
else
- ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops
+ database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops
end
end
@@ -389,7 +393,7 @@ module Mastodon::CLI
remove_index_if_exists!(:custom_emojis, 'index_custom_emojis_on_shortcode_and_domain')
say 'Deduplicating custom_emojis…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row|
emojis = CustomEmoji.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_emoji = emojis.shift
@@ -401,14 +405,14 @@ module Mastodon::CLI
end
say 'Restoring custom_emojis indexes…'
- ActiveRecord::Base.connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true
+ database_connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true
end
def deduplicate_custom_emoji_categories!
remove_index_if_exists!(:custom_emoji_categories, 'index_custom_emoji_categories_on_name')
say 'Deduplicating custom_emoji_categories…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row|
categories = CustomEmojiCategory.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_category = categories.shift
@@ -420,26 +424,26 @@ module Mastodon::CLI
end
say 'Restoring custom_emoji_categories indexes…'
- ActiveRecord::Base.connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true
+ database_connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true
end
def deduplicate_domain_allows!
remove_index_if_exists!(:domain_allows, 'index_domain_allows_on_domain')
say 'Deduplicating domain_allows…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row|
DomainAllow.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring domain_allows indexes…'
- ActiveRecord::Base.connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true
+ database_connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true
end
def deduplicate_domain_blocks!
remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain')
say 'Deduplicating domain_blocks…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a
reject_media = domain_blocks.any?(&:reject_media?)
@@ -456,49 +460,49 @@ module Mastodon::CLI
end
say 'Restoring domain_blocks indexes…'
- ActiveRecord::Base.connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true
+ database_connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true
end
def deduplicate_unavailable_domains!
- return unless ActiveRecord::Base.connection.table_exists?(:unavailable_domains)
+ return unless db_table_exists?(:unavailable_domains)
remove_index_if_exists!(:unavailable_domains, 'index_unavailable_domains_on_domain')
say 'Deduplicating unavailable_domains…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row|
UnavailableDomain.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring unavailable_domains indexes…'
- ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true
+ database_connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true
end
def deduplicate_email_domain_blocks!
remove_index_if_exists!(:email_domain_blocks, 'index_email_domain_blocks_on_domain')
say 'Deduplicating email_domain_blocks…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).order(EmailDomainBlock.arel_table[:parent_id].asc.nulls_first).to_a
domain_blocks.drop(1).each(&:destroy)
end
say 'Restoring email_domain_blocks indexes…'
- ActiveRecord::Base.connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true
+ database_connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true
end
def deduplicate_media_attachments!
remove_index_if_exists!(:media_attachments, 'index_media_attachments_on_shortcode')
say 'Deduplicating media_attachments…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row|
MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil)
end
say 'Restoring media_attachments indexes…'
if migrator_version < 2022_03_10_060626
- ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true
+ database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true
else
- ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops
+ database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops
end
end
@@ -506,19 +510,19 @@ module Mastodon::CLI
remove_index_if_exists!(:preview_cards, 'index_preview_cards_on_url')
say 'Deduplicating preview_cards…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row|
PreviewCard.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring preview_cards indexes…'
- ActiveRecord::Base.connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true
+ database_connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true
end
def deduplicate_statuses!
remove_index_if_exists!(:statuses, 'index_statuses_on_uri')
say 'Deduplicating statuses…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
statuses = Status.where(id: row['ids'].split(',')).order(id: :asc).to_a
ref_status = statuses.shift
statuses.each do |status|
@@ -529,9 +533,9 @@ module Mastodon::CLI
say 'Restoring statuses indexes…'
if migrator_version < 2022_03_10_060706
- ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true
+ database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true
else
- ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops
+ database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops
end
end
@@ -540,7 +544,7 @@ module Mastodon::CLI
remove_index_if_exists!(:tags, 'index_tags_on_name_lower_btree')
say 'Deduplicating tags…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row|
tags = Tag.where(id: row['ids'].split(',')).order(Arel.sql('(usable::int + trendable::int + listable::int) desc')).to_a
ref_tag = tags.shift
tags.each do |tag|
@@ -551,38 +555,38 @@ module Mastodon::CLI
say 'Restoring tags indexes…'
if migrator_version < 2021_04_21_121431
- ActiveRecord::Base.connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true
+ database_connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true
else
- ActiveRecord::Base.connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)'
+ database_connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)'
end
end
def deduplicate_webauthn_credentials!
- return unless ActiveRecord::Base.connection.table_exists?(:webauthn_credentials)
+ return unless db_table_exists?(:webauthn_credentials)
remove_index_if_exists!(:webauthn_credentials, 'index_webauthn_credentials_on_external_id')
say 'Deduplicating webauthn_credentials…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row|
WebauthnCredential.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end
say 'Restoring webauthn_credentials indexes…'
- ActiveRecord::Base.connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true
+ database_connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true
end
def deduplicate_webhooks!
- return unless ActiveRecord::Base.connection.table_exists?(:webhooks)
+ return unless db_table_exists?(:webhooks)
remove_index_if_exists!(:webhooks, 'index_webhooks_on_url')
say 'Deduplicating webhooks…'
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row|
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row|
Webhook.where(id: row['ids'].split(',')).order(id: :desc).drop(1).each(&:destroy)
end
say 'Restoring webhooks indexes…'
- ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true
+ database_connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true
end
def deduplicate_software_updates!
@@ -672,7 +676,7 @@ module Mastodon::CLI
def merge_statuses!(main_status, duplicate_status)
owned_classes = [Favourite, Mention, Poll]
- owned_classes << Bookmark if ActiveRecord::Base.connection.table_exists?(:bookmarks)
+ owned_classes << Bookmark if db_table_exists?(:bookmarks)
owned_classes.each do |klass|
klass.where(status_id: duplicate_status.id).find_each do |record|
record.update_attribute(:status_id, main_status.id)
@@ -715,13 +719,25 @@ module Mastodon::CLI
end
def find_duplicate_accounts
- ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1")
+ database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1")
end
def remove_index_if_exists!(table, name)
- ActiveRecord::Base.connection.remove_index(table, name: name) if ActiveRecord::Base.connection.index_name_exists?(table, name)
+ database_connection.remove_index(table, name: name) if database_connection.index_name_exists?(table, name)
rescue ArgumentError, ActiveRecord::StatementInvalid
nil
end
+
+ def database_connection
+ ActiveRecord::Base.connection
+ end
+
+ def db_table_exists?(table)
+ database_connection.table_exists?(table)
+ end
+
+ def rebuild_index(name)
+ database_connection.execute("REINDEX INDEX #{name}")
+ end
end
end
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
index 5b4d385465..61b52e1c80 100644
--- a/lib/tasks/tests.rake
+++ b/lib/tasks/tests.rake
@@ -2,6 +2,22 @@
namespace :tests do
namespace :migrations do
+ desc 'Prepares all migrations and test data for consistency checks'
+ task prepare_database: :environment do
+ {
+ '2' => 2017_10_10_025614,
+ '2_4' => 2018_05_14_140000,
+ '2_4_3' => 2018_07_07_154237,
+ }.each do |release, version|
+ ActiveRecord::Tasks::DatabaseTasks
+ .migration_connection
+ .migration_context
+ .migrate(version)
+ Rake::Task["tests:migrations:populate_v#{release}"]
+ .invoke
+ end
+ end
+
desc 'Check that database state is consistent with a successful migration from populated data'
task check_database: :environment do
unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false
@@ -88,6 +104,8 @@ namespace :tests do
puts 'Locale for fr-QC users not updated to fr-CA as expected'
exit(1)
end
+
+ puts 'No errors found. Database state is consistent with a successful migration process.'
end
desc 'Populate the database with test data for 2.4.3'
diff --git a/spec/features/redirections_spec.rb b/spec/features/redirections_spec.rb
new file mode 100644
index 0000000000..f73ab58470
--- /dev/null
+++ b/spec/features/redirections_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'redirection confirmations' do
+ let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/foo', url: 'https://example.com/@foo') }
+ let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/users/foo/statuses/1', url: 'https://example.com/@foo/1') }
+
+ context 'when a logged out user visits a local page for a remote account' do
+ it 'shows a confirmation page' do
+ visit "/@#{account.pretty_acct}"
+
+ # It explains about the redirect
+ expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io'))
+
+ # It features an appropriate link
+ expect(page).to have_link(account.url, href: account.url)
+ end
+ end
+
+ context 'when a logged out user visits a local page for a remote status' do
+ it 'shows a confirmation page' do
+ visit "/@#{account.pretty_acct}/#{status.id}"
+
+ # It explains about the redirect
+ expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io'))
+
+ # It features an appropriate link
+ expect(page).to have_link(status.url, href: status.url)
+ end
+ end
+end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index f7c2ec69b9..a8e81fee00 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -1933,6 +1933,49 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
+ context 'when object URI uses bearcaps' do
+ subject { described_class.new(json, sender) }
+
+ let(:token) { 'foo' }
+
+ let(:json) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
+ type: 'Create',
+ actor: ActivityPub::TagManager.instance.uri_for(sender),
+ object: Addressable::URI.new(scheme: 'bear', query_values: { t: token, u: object_json[:id] }).to_s,
+ }.with_indifferent_access
+ end
+
+ let(:object_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+ type: 'Note',
+ content: 'Lorem ipsum',
+ to: 'https://www.w3.org/ns/activitystreams#Public',
+ }
+ end
+
+ before do
+ stub_request(:get, object_json[:id])
+ .with(headers: { Authorization: "Bearer #{token}" })
+ .to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' })
+
+ subject.perform
+ end
+
+ it 'creates status' do
+ status = sender.statuses.first
+
+ expect(status).to_not be_nil
+ expect(status).to have_attributes(
+ visibility: 'public',
+ text: 'Lorem ipsum'
+ )
+ end
+ end
+
context 'with an encrypted message' do
subject { described_class.new(json, sender, delivery: true, delivered_to_account_id: recipient.id) }
@@ -2182,54 +2225,5 @@ RSpec.describe ActivityPub::Activity::Create do
expect(sender.statuses.count).to eq 0
end
end
-
- context 'when bearcaps' do
- subject { described_class.new(json, sender) }
-
- before do
- stub_request(:get, 'https://example.com/statuses/1234567890')
- .with(headers: { 'Authorization' => 'Bearer test_ohagi_token' })
- .to_return(status: 200, body: Oj.dump(object_json), headers: {})
-
- subject.perform
- end
-
- let!(:recipient) { Fabricate(:account) }
- let(:object_json) do
- {
- id: 'https://example.com/statuses/1234567890',
- type: 'Note',
- content: 'Lorem ipsum',
- to: ActivityPub::TagManager.instance.uri_for(recipient),
- attachment: [
- {
- type: 'Document',
- mediaType: 'image/png',
- url: 'http://example.com/attachment.png',
- },
- ],
- }
- end
- let(:json) do
- {
- '@context': 'https://www.w3.org/ns/activitystreams',
- id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
- type: 'Create',
- actor: ActivityPub::TagManager.instance.uri_for(sender),
- object: "bear:?#{{ u: 'https://example.com/statuses/1234567890', t: 'test_ohagi_token' }.to_query}",
- }.with_indifferent_access
- end
-
- it 'creates status' do
- status = sender.statuses.first
-
- expect(status).to_not be_nil
- expect(status.text).to eq 'Lorem ipsum'
- expect(status.mentions.map(&:account)).to include(recipient)
- expect(status.mentions.count).to eq 1
- expect(status.visibility).to eq 'limited'
- expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
- end
- end
end
end
diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb
index da2a774b2d..a08fd723a4 100644
--- a/spec/models/account_statuses_cleanup_policy_spec.rb
+++ b/spec/models/account_statuses_cleanup_policy_spec.rb
@@ -296,16 +296,11 @@ RSpec.describe AccountStatusesCleanupPolicy do
let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) }
let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) }
- it 'returns statuses including max_id' do
- expect(subject).to include(old_status.id)
- end
-
- it 'returns statuses including older than max_id' do
- expect(subject).to include(very_old_status.id)
- end
-
- it 'does not return statuses newer than max_id' do
- expect(subject).to_not include(slightly_less_old_status.id)
+ it 'returns statuses included the max_id and older than the max_id but not newer than max_id' do
+ expect(subject)
+ .to include(old_status.id)
+ .and include(very_old_status.id)
+ .and not_include(slightly_less_old_status.id)
end
end
@@ -315,16 +310,11 @@ RSpec.describe AccountStatusesCleanupPolicy do
let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) }
let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) }
- it 'returns statuses including min_id' do
- expect(subject).to include(old_status.id)
- end
-
- it 'returns statuses including newer than max_id' do
- expect(subject).to include(slightly_less_old_status.id)
- end
-
- it 'does not return statuses older than min_id' do
- expect(subject).to_not include(very_old_status.id)
+ it 'returns statuses including min_id and newer than min_id, but not older than min_id' do
+ expect(subject)
+ .to include(old_status.id)
+ .and include(slightly_less_old_status.id)
+ .and not_include(very_old_status.id)
end
end
@@ -339,12 +329,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.min_status_age = 2.years.seconds
end
- it 'does not return unrelated old status' do
- expect(subject.pluck(:id)).to_not include(unrelated_status.id)
- end
-
- it 'returns only oldest status for deletion' do
- expect(subject.pluck(:id)).to eq [very_old_status.id]
+ it 'does not return unrelated old status and does return oldest status' do
+ expect(subject.pluck(:id))
+ .to not_include(unrelated_status.id)
+ .and eq [very_old_status.id]
end
end
@@ -358,12 +346,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the old direct message for deletion' do
- expect(subject.pluck(:id)).to_not include(direct_message.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status except does not return the old direct message for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(direct_message.id)
+ .and include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -377,12 +363,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = true
end
- it 'does not return the old self-bookmarked message for deletion' do
- expect(subject.pluck(:id)).to_not include(self_bookmarked.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the old self-bookmarked message for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(self_bookmarked.id)
+ .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -396,12 +380,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the old self-bookmarked message for deletion' do
- expect(subject.pluck(:id)).to_not include(self_faved.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the old self-faved message for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(self_faved.id)
+ .and include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -415,12 +397,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the old message with media for deletion' do
- expect(subject.pluck(:id)).to_not include(status_with_media.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the old message with media for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(status_with_media.id)
+ .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -434,12 +414,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the old poll message for deletion' do
- expect(subject.pluck(:id)).to_not include(status_with_poll.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the old poll message for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(status_with_poll.id)
+ .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -453,12 +431,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the old pinned message for deletion' do
- expect(subject.pluck(:id)).to_not include(pinned_status.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the old pinned message for deletion' do
+ expect(subject.pluck(:id))
+ .to not_include(pinned_status.id)
+ .and include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -472,16 +448,11 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = false
end
- it 'does not return the recent toot' do
- expect(subject.pluck(:id)).to_not include(recent_status.id)
- end
-
- it 'does not return the unrelated toot' do
- expect(subject.pluck(:id)).to_not include(unrelated_status.id)
- end
-
- it 'returns every other old status for deletion' do
- expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns every old status but does not return the recent or unrelated statuses' do
+ expect(subject.pluck(:id))
+ .to not_include(recent_status.id)
+ .and not_include(unrelated_status.id)
+ .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -495,12 +466,10 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.keep_self_bookmark = true
end
- it 'does not return unrelated old status' do
- expect(subject.pluck(:id)).to_not include(unrelated_status.id)
- end
-
- it 'returns only normal statuses for deletion' do
- expect(subject.pluck(:id)).to contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns normal statuses and does not return unrelated old status' do
+ expect(subject.pluck(:id))
+ .to not_include(unrelated_status.id)
+ .and contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
@@ -509,20 +478,12 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.min_reblogs = 5
end
- it 'does not return the recent toot' do
- expect(subject.pluck(:id)).to_not include(recent_status.id)
- end
-
- it 'does not return the toot reblogged 5 times' do
- expect(subject.pluck(:id)).to_not include(reblogged_secondary.id)
- end
-
- it 'does not return the unrelated toot' do
- expect(subject.pluck(:id)).to_not include(unrelated_status.id)
- end
-
- it 'returns old statuses not reblogged as much' do
- expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id)
+ it 'returns old not-reblogged statuses but does not return the recent, 5-times reblogged, or unrelated statuses' do
+ expect(subject.pluck(:id))
+ .to not_include(recent_status.id)
+ .and not_include(reblogged_secondary.id)
+ .and not_include(unrelated_status.id)
+ .and include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id)
end
end
@@ -531,20 +492,12 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.min_favs = 5
end
- it 'does not return the recent toot' do
- expect(subject.pluck(:id)).to_not include(recent_status.id)
- end
-
- it 'does not return the toot faved 5 times' do
- expect(subject.pluck(:id)).to_not include(faved_secondary.id)
- end
-
- it 'does not return the unrelated toot' do
- expect(subject.pluck(:id)).to_not include(unrelated_status.id)
- end
-
- it 'returns old statuses not faved as much' do
- expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id)
+ it 'returns old not-faved statuses but does not return the recent, 5-times faved, or unrelated statuses' do
+ expect(subject.pluck(:id))
+ .to not_include(recent_status.id)
+ .and not_include(faved_secondary.id)
+ .and not_include(unrelated_status.id)
+ .and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id)
end
end
end
diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb
new file mode 100644
index 0000000000..4e3ab060a0
--- /dev/null
+++ b/spec/models/custom_filter_keyword_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe CustomFilterKeyword do
+ describe '#to_regex' do
+ context 'when whole_word is true' do
+ it 'builds a regex with boundaries and the keyword' do
+ keyword = described_class.new(whole_word: true, keyword: 'test')
+
+ expect(keyword.to_regex).to eq(/(?mix:\b#{Regexp.escape(keyword.keyword)}\b)/)
+ end
+
+ it 'builds a regex with starting boundary and the keyword when end with non-word' do
+ keyword = described_class.new(whole_word: true, keyword: 'test#')
+
+ expect(keyword.to_regex).to eq(/(?mix:\btest\#)/)
+ end
+
+ it 'builds a regex with end boundary and the keyword when start with non-word' do
+ keyword = described_class.new(whole_word: true, keyword: '#test')
+
+ expect(keyword.to_regex).to eq(/(?mix:\#test\b)/)
+ end
+ end
+
+ context 'when whole_word is false' do
+ it 'builds a regex with the keyword' do
+ keyword = described_class.new(whole_word: false, keyword: 'test')
+
+ expect(keyword.to_regex).to eq(/test/i)
+ end
+ end
+ end
+end
diff --git a/spec/models/instance_spec.rb b/spec/models/instance_spec.rb
new file mode 100644
index 0000000000..3e811d3325
--- /dev/null
+++ b/spec/models/instance_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Instance do
+ describe 'Scopes' do
+ before { described_class.refresh }
+
+ describe '#searchable' do
+ let(:expected_domain) { 'host.example' }
+ let(:blocked_domain) { 'other.example' }
+
+ before do
+ Fabricate :account, domain: expected_domain
+ Fabricate :account, domain: blocked_domain
+ Fabricate :domain_block, domain: blocked_domain
+ end
+
+ it 'returns records not domain blocked' do
+ results = described_class.searchable.pluck(:domain)
+
+ expect(results)
+ .to include(expected_domain)
+ .and not_include(blocked_domain)
+ end
+ end
+
+ describe '#matches_domain' do
+ let(:host_domain) { 'host.example.com' }
+ let(:host_under_domain) { 'host_under.example.com' }
+ let(:other_domain) { 'other.example' }
+
+ before do
+ Fabricate :account, domain: host_domain
+ Fabricate :account, domain: host_under_domain
+ Fabricate :account, domain: other_domain
+ end
+
+ it 'returns matching records' do
+ expect(described_class.matches_domain('host.exa').pluck(:domain))
+ .to include(host_domain)
+ .and not_include(other_domain)
+
+ expect(described_class.matches_domain('ple.com').pluck(:domain))
+ .to include(host_domain)
+ .and not_include(other_domain)
+
+ expect(described_class.matches_domain('example').pluck(:domain))
+ .to include(host_domain)
+ .and include(other_domain)
+
+ expect(described_class.matches_domain('host_').pluck(:domain)) # Preserve SQL wildcards
+ .to include(host_domain)
+ .and include(host_under_domain)
+ .and not_include(other_domain)
+ end
+ end
+
+ describe '#by_domain_and_subdomains' do
+ let(:exact_match_domain) { 'example.com' }
+ let(:subdomain_domain) { 'foo.example.com' }
+ let(:partial_domain) { 'grexample.com' }
+
+ before do
+ Fabricate(:account, domain: exact_match_domain)
+ Fabricate(:account, domain: subdomain_domain)
+ Fabricate(:account, domain: partial_domain)
+ end
+
+ it 'returns matching instances' do
+ results = described_class.by_domain_and_subdomains('example.com').pluck(:domain)
+
+ expect(results)
+ .to include(exact_match_domain)
+ .and include(subdomain_domain)
+ .and not_include(partial_domain)
+ end
+ end
+
+ describe '#with_domain_follows' do
+ let(:example_domain) { 'example.host' }
+ let(:other_domain) { 'other.host' }
+ let(:none_domain) { 'none.host' }
+
+ before do
+ example_account = Fabricate(:account, domain: example_domain)
+ other_account = Fabricate(:account, domain: other_domain)
+ Fabricate(:account, domain: none_domain)
+
+ Fabricate :follow, account: example_account
+ Fabricate :follow, target_account: other_account
+ end
+
+ it 'returns instances with domain accounts that have follows' do
+ results = described_class.with_domain_follows(['example.host', 'other.host', 'none.host']).pluck(:domain)
+
+ expect(results)
+ .to include(example_domain)
+ .and include(other_domain)
+ .and not_include(none_domain)
+ end
+ end
+ end
+end
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index f4a2b8fec6..63502c546e 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -265,7 +265,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
anything
)
- expect(Status.where(uri: 'https://example.com/users/bob/fake-status').exists?).to be false
+ expect(Status.exists?(uri: 'https://example.com/users/bob/fake-status')).to be false
end
end
end
diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb
index a3b816b4d5..5a3f1b406b 100644
--- a/spec/system/new_statuses_spec.rb
+++ b/spec/system/new_statuses_spec.rb
@@ -24,7 +24,7 @@ describe 'NewStatuses', :sidekiq_inline do
within('.compose-form') do
fill_in "What's on your mind?", with: status_text
- click_on 'Publish!'
+ click_on 'Post'
end
expect(subject).to have_css('.status__content__text', text: status_text)
@@ -37,7 +37,7 @@ describe 'NewStatuses', :sidekiq_inline do
within('.compose-form') do
fill_in "What's on your mind?", with: status_text
- click_on 'Publish!'
+ click_on 'Post'
end
expect(subject).to have_css('.status__content__text', text: status_text)
diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb
index fd02d1120c..126a816bcc 100644
--- a/spec/system/share_entrypoint_spec.rb
+++ b/spec/system/share_entrypoint_spec.rb
@@ -19,13 +19,13 @@ describe 'ShareEntrypoint' do
it 'can be used to post a new status' do
expect(subject).to have_css('div#mastodon-compose')
- expect(subject).to have_css('.compose-form__publish-button-wrapper > button')
+ expect(subject).to have_css('.compose-form__submit')
status_text = 'This is a new status!'
within('.compose-form') do
fill_in "What's on your mind?", with: status_text
- click_on 'Publish!'
+ click_on 'Post'
end
expect(subject).to have_css('.notification-bar-message', text: 'Post published.')
diff --git a/streaming/index.js b/streaming/index.js
index 356ad42eb7..dbe32af23f 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -8,7 +8,7 @@ const url = require('url');
const cors = require('cors');
const dotenv = require('dotenv');
const express = require('express');
-const Redis = require('ioredis');
+const { Redis } = require('ioredis');
const { JSDOM } = require('jsdom');
const pg = require('pg');
const dbUrlToConfig = require('pg-connection-string').parse;
@@ -43,13 +43,18 @@ initializeLogLevel(process.env, environment);
*/
/**
- * @param {Object.
} config
+ * @param {RedisConfiguration} config
+ * @returns {Promise}
*/
-const createRedisClient = async (config) => {
- const { redisParams, redisUrl } = config;
- // @ts-ignore
- const client = new Redis(redisUrl, redisParams);
- // @ts-ignore
+const createRedisClient = async ({ redisParams, redisUrl }) => {
+ let client;
+
+ if (typeof redisUrl === 'string') {
+ client = new Redis(redisUrl, redisParams);
+ } else {
+ client = new Redis(redisParams);
+ }
+
client.on('error', (err) => logger.error({ err }, 'Redis Client Error!'));
return client;
@@ -87,39 +92,101 @@ const parseJSON = (json, req) => {
};
/**
- * @param {Object.} env the `process.env` value to read configuration from
- * @returns {Object.} the configuration for the PostgreSQL connection
+ * Takes an environment variable that should be an integer, attempts to parse
+ * it falling back to a default if not set, and handles errors parsing.
+ * @param {string|undefined} value
+ * @param {number} defaultValue
+ * @param {string} variableName
+ * @returns {number}
+ */
+const parseIntFromEnv = (value, defaultValue, variableName) => {
+ if (typeof value === 'string' && value.length > 0) {
+ const parsedValue = parseInt(value, 10);
+ if (isNaN(parsedValue)) {
+ throw new Error(`Invalid ${variableName} environment variable: ${value}`);
+ }
+ return parsedValue;
+ } else {
+ return defaultValue;
+ }
+};
+
+/**
+ * @param {NodeJS.ProcessEnv} env the `process.env` value to read configuration from
+ * @returns {pg.PoolConfig} the configuration for the PostgreSQL connection
*/
const pgConfigFromEnv = (env) => {
+ /** @type {Record} */
const pgConfigs = {
development: {
- user: env.DB_USER || pg.defaults.user,
+ user: env.DB_USER || pg.defaults.user,
password: env.DB_PASS || pg.defaults.password,
database: env.DB_NAME || 'mastodon_development',
- host: env.DB_HOST || pg.defaults.host,
- port: env.DB_PORT || pg.defaults.port,
+ host: env.DB_HOST || pg.defaults.host,
+ port: parseIntFromEnv(env.DB_PORT, pg.defaults.port ?? 5432, 'DB_PORT')
},
production: {
- user: env.DB_USER || 'mastodon',
+ user: env.DB_USER || 'mastodon',
password: env.DB_PASS || '',
database: env.DB_NAME || 'mastodon_production',
- host: env.DB_HOST || 'localhost',
- port: env.DB_PORT || 5432,
+ host: env.DB_HOST || 'localhost',
+ port: parseIntFromEnv(env.DB_PORT, 5432, 'DB_PORT')
},
};
- let baseConfig;
+ /**
+ * @type {pg.PoolConfig}
+ */
+ let baseConfig = {};
if (env.DATABASE_URL) {
- baseConfig = dbUrlToConfig(env.DATABASE_URL);
+ const parsedUrl = dbUrlToConfig(env.DATABASE_URL);
+
+ // The result of dbUrlToConfig from pg-connection-string is not type
+ // compatible with pg.PoolConfig, since parts of the connection URL may be
+ // `null` when pg.PoolConfig expects `undefined`, as such we have to
+ // manually create the baseConfig object from the properties of the
+ // parsedUrl.
+ //
+ // For more information see:
+ // https://github.com/brianc/node-postgres/issues/2280
+ //
+ // FIXME: clean up once brianc/node-postgres#3128 lands
+ if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password;
+ if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host;
+ if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user;
+ if (typeof parsedUrl.port === 'string') {
+ const parsedPort = parseInt(parsedUrl.port, 10);
+ if (isNaN(parsedPort)) {
+ throw new Error('Invalid port specified in DATABASE_URL environment variable');
+ }
+ baseConfig.port = parsedPort;
+ }
+ if (typeof parsedUrl.database === 'string') baseConfig.database = parsedUrl.database;
+ if (typeof parsedUrl.options === 'string') baseConfig.options = parsedUrl.options;
+
+ // The pg-connection-string type definition isn't correct, as parsedUrl.ssl
+ // can absolutely be an Object, this is to work around these incorrect
+ // types, including the casting of parsedUrl.ssl to Record
+ if (typeof parsedUrl.ssl === 'boolean') {
+ baseConfig.ssl = parsedUrl.ssl;
+ } else if (typeof parsedUrl.ssl === 'object' && !Array.isArray(parsedUrl.ssl) && parsedUrl.ssl !== null) {
+ /** @type {Record} */
+ const sslOptions = parsedUrl.ssl;
+ baseConfig.ssl = {};
+
+ baseConfig.ssl.cert = sslOptions.cert;
+ baseConfig.ssl.key = sslOptions.key;
+ baseConfig.ssl.ca = sslOptions.ca;
+ baseConfig.ssl.rejectUnauthorized = sslOptions.rejectUnauthorized;
+ }
// Support overriding the database password in the connection URL
if (!baseConfig.password && env.DB_PASS) {
baseConfig.password = env.DB_PASS;
}
- } else {
- // @ts-ignore
+ } else if (Object.hasOwnProperty.call(pgConfigs, environment)) {
baseConfig = pgConfigs[environment];
if (env.DB_SSLMODE) {
@@ -136,42 +203,58 @@ const pgConfigFromEnv = (env) => {
break;
}
}
+ } else {
+ throw new Error('Unable to resolve postgresql database configuration.');
}
return {
...baseConfig,
- max: env.DB_POOL || 10,
+ max: parseIntFromEnv(env.DB_POOL, 10, 'DB_POOL'),
connectionTimeoutMillis: 15000,
+ // Deliberately set application_name to an empty string to prevent excessive
+ // CPU usage with PG Bouncer. See:
+ // - https://github.com/mastodon/mastodon/pull/23958
+ // - https://github.com/pgbouncer/pgbouncer/issues/349
application_name: '',
};
};
/**
- * @param {Object.} env the `process.env` value to read configuration from
- * @returns {Object.} configuration for the Redis connection
+ * @typedef RedisConfiguration
+ * @property {import('ioredis').RedisOptions} redisParams
+ * @property {string} redisPrefix
+ * @property {string|undefined} redisUrl
+ */
+
+/**
+ * @param {NodeJS.ProcessEnv} env the `process.env` value to read configuration from
+ * @returns {RedisConfiguration} configuration for the Redis connection
*/
const redisConfigFromEnv = (env) => {
// ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*,
// which means we can't use it. But this is something that should be looked into.
const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : '';
+ let redisPort = parseIntFromEnv(env.REDIS_PORT, 6379, 'REDIS_PORT');
+ let redisDatabase = parseIntFromEnv(env.REDIS_DB, 0, 'REDIS_DB');
+
+ /** @type {import('ioredis').RedisOptions} */
const redisParams = {
host: env.REDIS_HOST || '127.0.0.1',
- port: env.REDIS_PORT || 6379,
- db: env.REDIS_DB || 0,
+ port: redisPort,
+ db: redisDatabase,
password: env.REDIS_PASSWORD || undefined,
};
// redisParams.path takes precedence over host and port.
if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
- // @ts-ignore
redisParams.path = env.REDIS_URL.slice(7);
}
return {
redisParams,
redisPrefix,
- redisUrl: env.REDIS_URL,
+ redisUrl: typeof env.REDIS_URL === 'string' ? env.REDIS_URL : undefined,
};
};
diff --git a/yarn.lock b/yarn.lock
index fbc6740898..8e9a5deb57 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4643,13 +4643,13 @@ __metadata:
linkType: hard
"axios@npm:^1.4.0":
- version: 1.6.5
- resolution: "axios@npm:1.6.5"
+ version: 1.6.6
+ resolution: "axios@npm:1.6.6"
dependencies:
follow-redirects: "npm:^1.15.4"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
- checksum: aeb9acf87590d8aa67946072ced38e01ca71f5dfe043782c0ccea667e5dd5c45830c08afac9be3d7c894f09684b8ab2a458f497d197b73621233bcf202d9d468
+ checksum: 974f54cfade94fd4c0191309122a112c8d233089cecb0070cd8e0904e9bd9c364ac3a6fd0f981c978508077249788950427c565f54b7b2110e5c3426006ff343
languageName: node
linkType: hard
@@ -6826,9 +6826,9 @@ __metadata:
linkType: hard
"dotenv@npm:^16.0.3":
- version: 16.4.0
- resolution: "dotenv@npm:16.4.0"
- checksum: 70c3b422cefaffdba300aecd9157668590c3b5e66efb3742b7dec207f85023e5997364f04030fc0393fae52bf3a874979632d289ab4fafc1386ff2c68f2f2e8d
+ version: 16.4.1
+ resolution: "dotenv@npm:16.4.1"
+ checksum: ef3d95f48f38146df0881a4b58447ae437d2da3f6d645074b84de4e64ef64ba75fc357c5ed66b3c2b813b5369fdeb6a4777d6ade2d50e54eed6aa06dddc98bc4
languageName: node
linkType: hard