diff --git a/Gemfile.lock b/Gemfile.lock index d8a13e8ecf..ed217ca146 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,16 +100,16 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.977.0) - aws-sdk-core (3.208.0) + aws-partitions (1.978.0) + aws-sdk-core (3.209.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.93.0) + aws-sdk-kms (1.94.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.165.0) + aws-sdk-s3 (1.166.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index d43a658421..f930a4510e 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -31,7 +31,7 @@ module WebAppControllerConcern def redirect_unauthenticated_to_permalinks! return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in - permalink_redirector = PermalinkRedirector.new(request.path) + permalink_redirector = PermalinkRedirector.new(request.original_fullpath) return if permalink_redirector.redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 9d3fc0d425..b40b04f8cc 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -68,10 +68,15 @@ function dispatchAssociatedRecords( dispatch(importFetchedStatuses(fetchedStatuses)); } +const supportedGroupedNotificationTypes = ['favourite', 'reblog']; + export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', async (_params, { getState }) => - apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }), + apiFetchNotificationGroups({ + grouped_types: supportedGroupedNotificationTypes, + exclude_types: getExcludedTypes(getState()), + }), ({ notifications, accounts, statuses }, { dispatch }) => { dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedStatuses(statuses)); @@ -93,6 +98,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( 'notificationGroups/fetchGap', async (params: { gap: NotificationGap }, { getState }) => apiFetchNotificationGroups({ + grouped_types: supportedGroupedNotificationTypes, max_id: params.gap.maxId, exclude_types: getExcludedTypes(getState()), }), @@ -109,6 +115,7 @@ export const pollRecentNotifications = createDataLoadingThunk( 'notificationGroups/pollRecentNotifications', async (_params, { getState }) => { return apiFetchNotificationGroups({ + grouped_types: supportedGroupedNotificationTypes, max_id: undefined, exclude_types: getExcludedTypes(getState()), // In slow mode, we don't want to include notifications that duplicate the already-displayed ones diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts index 92863ac5ca..813e2f3a17 100644 --- a/app/javascript/mastodon/api/notifications.ts +++ b/app/javascript/mastodon/api/notifications.ts @@ -31,6 +31,7 @@ export const apiFetchNotifications = async ( export const apiFetchNotificationGroups = async (params?: { url?: string; + grouped_types?: string[]; exclude_types?: string[]; max_id?: string; since_id?: string; diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 165e81c7d8..75531abf56 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -375,20 +375,29 @@ class StatusActionBar extends ImmutablePureComponent { return (
- - - - - - +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
); } diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index a6e3640478..2f9f962b81 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -186,7 +186,7 @@ class SwitchingColumnsArea extends PureComponent { {redirect} {singleColumn ? : null} - {singleColumn && pathName.startsWith('/deck/') ? : null} + {singleColumn && pathName.startsWith('/deck/') ? : null} {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} {!singleColumn && pathName === '/getting-started' ? : null} {!singleColumn && pathName === '/home' ? : null} diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 27e776024f..ead016dec2 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -164,7 +164,7 @@ "compose_form.publish": "Publier", "compose_form.publish_form": "Publier", "compose_form.reply": "Répondre", - "compose_form.save_changes": "Mis à jour", + "compose_form.save_changes": "Mettre à jour", "compose_form.spoiler.marked": "Enlever l'avertissement de contenu", "compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu", "compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index f787216c46..9375eccc6f 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -164,7 +164,7 @@ "compose_form.publish": "Publier", "compose_form.publish_form": "Nouvelle publication", "compose_form.reply": "Répondre", - "compose_form.save_changes": "Mis à jour", + "compose_form.save_changes": "Mettre à jour", "compose_form.spoiler.marked": "Enlever l’avertissement de contenu", "compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu", "compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 74c46c7b33..04b73e78fa 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -76,7 +76,7 @@ "admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký", "admin.dashboard.retention.average": "Trung bình", "admin.dashboard.retention.cohort": "Tháng đăng ký", - "admin.dashboard.retention.cohort_size": "Người mới", + "admin.dashboard.retention.cohort_size": "Số người", "admin.impact_report.instance_accounts": "Hồ sơ tài khoản này sẽ xóa", "admin.impact_report.instance_followers": "Người theo dõi của thành viên máy chủ sẽ mất", "admin.impact_report.instance_follows": "Người theo dõi người dùng của họ sẽ mất", @@ -154,7 +154,7 @@ "compose_form.lock_disclaimer": "Tài khoản của bạn không {locked}. Bất cứ ai cũng có thể theo dõi và xem tút riêng tư của bạn.", "compose_form.lock_disclaimer.lock": "khóa", "compose_form.placeholder": "Bạn đang nghĩ gì?", - "compose_form.poll.duration": "Hết hạn", + "compose_form.poll.duration": "Hết hạn sau", "compose_form.poll.multiple": "Chọn nhiều", "compose_form.poll.option_placeholder": "Lựa chọn {number}", "compose_form.poll.single": "Chọn một", @@ -180,7 +180,7 @@ "confirmations.discard_edit_media.message": "Bạn chưa lưu thay đổi đối với phần mô tả hoặc bản xem trước của media, vẫn bỏ luôn?", "confirmations.edit.confirm": "Sửa", "confirmations.edit.message": "Nội dung tút cũ sẽ bị ghi đè, bạn có tiếp tục?", - "confirmations.edit.title": "Viết đè lên tút cũ", + "confirmations.edit.title": "Ghi đè lên tút cũ", "confirmations.logout.confirm": "Đăng xuất", "confirmations.logout.message": "Bạn có chắc muốn thoát?", "confirmations.logout.title": "Đăng xuất", @@ -190,11 +190,11 @@ "confirmations.redraft.title": "Xóa & viết lại", "confirmations.reply.confirm": "Trả lời", "confirmations.reply.message": "Nội dung bạn đang soạn thảo sẽ bị ghi đè, bạn có tiếp tục?", - "confirmations.reply.title": "Viết đè lên tút cũ", + "confirmations.reply.title": "Ghi đè lên tút cũ", "confirmations.unfollow.confirm": "Bỏ theo dõi", "confirmations.unfollow.message": "Bạn có chắc muốn bỏ theo dõi {name}?", "confirmations.unfollow.title": "Bỏ theo dõi", - "content_warning.hide": "Ẩn tút", + "content_warning.hide": "Ẩn lại", "content_warning.show": "Nhấn để xem", "conversation.delete": "Xóa tin nhắn này", "conversation.mark_as_read": "Đánh dấu là đã đọc", @@ -322,7 +322,7 @@ "follow_suggestions.hints.most_interactions": "Người này đang thu hút sự chú ý trên {domain}.", "follow_suggestions.hints.similar_to_recently_followed": "Người này có nét giống những người mà bạn theo dõi gần đây.", "follow_suggestions.personalized_suggestion": "Gợi ý cá nhân hóa", - "follow_suggestions.popular_suggestion": "Những người nổi tiếng", + "follow_suggestions.popular_suggestion": "Người nổi tiếng", "follow_suggestions.popular_suggestion_longer": "Nổi tiếng trên {domain}", "follow_suggestions.similar_to_recently_followed_longer": "Tương tự những người mà bạn theo dõi gần đây", "follow_suggestions.view_all": "Xem tất cả", @@ -480,7 +480,7 @@ "navigation_bar.domain_blocks": "Máy chủ đã ẩn", "navigation_bar.explore": "Xu hướng", "navigation_bar.favourites": "Tút thích", - "navigation_bar.filters": "Bộ lọc từ ngữ", + "navigation_bar.filters": "Từ khóa đã lọc", "navigation_bar.follow_requests": "Yêu cầu theo dõi", "navigation_bar.followed_tags": "Hashtag theo dõi", "navigation_bar.follows_and_followers": "Quan hệ", @@ -555,7 +555,7 @@ "notification_requests.view": "Hiện thông báo", "notifications.clear": "Xóa hết thông báo", "notifications.clear_confirmation": "Bạn có chắc muốn xóa vĩnh viễn tất cả thông báo của mình?", - "notifications.clear_title": "Xóa hết thông báo?", + "notifications.clear_title": "Xóa toàn bộ thông báo", "notifications.column_settings.admin.report": "Báo cáo mới:", "notifications.column_settings.admin.sign_up": "Người mới tham gia:", "notifications.column_settings.alert": "Báo trên máy tính", @@ -601,8 +601,8 @@ "notifications.policy.filter_not_followers_title": "Những người không theo dõi bạn", "notifications.policy.filter_not_following_hint": "Cho tới khi bạn duyệt họ", "notifications.policy.filter_not_following_title": "Những người bạn không theo dõi", - "notifications.policy.filter_private_mentions_hint": "Được lọc trừ khi nó trả lời lượt nhắc từ bạn hoặc nếu bạn theo dõi người gửi", - "notifications.policy.filter_private_mentions_title": "Lượt nhắc riêng tư không được yêu cầu", + "notifications.policy.filter_private_mentions_hint": "Trừ khi nó trả lời lượt nhắc từ bạn hoặc nếu bạn có theo dõi người gửi", + "notifications.policy.filter_private_mentions_title": "Lượt nhắn riêng không mong muốn", "notifications.policy.title": "Quản lý thông báo từ…", "notifications_permission_banner.enable": "Cho phép thông báo trên màn hình", "notifications_permission_banner.how_to_control": "Hãy bật thông báo trên màn hình để không bỏ lỡ những thông báo từ Mastodon. Một khi đã bật, bạn có thể lựa chọn từng loại thông báo khác nhau thông qua {icon} nút bên dưới.", @@ -713,7 +713,7 @@ "report.reasons.other": "Một lý do khác", "report.reasons.other_description": "Vấn đề không nằm trong những mục trên", "report.reasons.spam": "Đây là spam", - "report.reasons.spam_description": "Liên kết độc hại, tạo tương tác giả hoặc trả lời lặp đi lặp lại", + "report.reasons.spam_description": "Liên kết độc hại, giả tương tác hoặc trả lời lặp đi lặp lại", "report.reasons.violation": "Vi phạm nội quy máy chủ", "report.reasons.violation_description": "Bạn nhận thấy nó vi phạm nội quy máy chủ", "report.rules.subtitle": "Chọn tất cả những gì phù hợp", @@ -787,9 +787,9 @@ "status.edit": "Sửa", "status.edited": "Sửa lần cuối {date}", "status.edited_x_times": "Đã sửa {count, plural, other {{count} lần}}", - "status.embed": "Lấy mã nhúng", + "status.embed": "Nhúng", "status.favourite": "Thích", - "status.favourites": "{count, plural, other {Thích}}", + "status.favourites": "{count, plural, other {thích}}", "status.filter": "Lọc tút này", "status.history.created": "{name} đăng {date}", "status.history.edited": "{name} đã sửa {date}", @@ -808,7 +808,7 @@ "status.reblog": "Đăng lại", "status.reblog_private": "Đăng lại (Riêng tư)", "status.reblogged_by": "{name} đăng lại", - "status.reblogs": "{count, plural, other {Đăng lại}}", + "status.reblogs": "{count, plural, other {đăng lại}}", "status.reblogs.empty": "Tút này chưa có ai đăng lại. Nếu có, nó sẽ hiển thị ở đây.", "status.redraft": "Xóa và viết lại", "status.remove_bookmark": "Bỏ lưu", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 55642e250f..896f8def64 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -93,7 +93,7 @@ &:disabled, &.disabled { background-color: $ui-primary-color; - cursor: default; + cursor: not-allowed; } &.copyable { @@ -299,6 +299,10 @@ } } + &--with-counter { + padding-inline-end: 4px; + } + &__counter { display: block; width: auto; @@ -1465,6 +1469,15 @@ body > [data-popper-placement] { } } + &__action-bar__button-wrapper { + flex-basis: 0; + flex-grow: 1; + + &:last-child { + flex-grow: 0; + } + } + &--first-in-thread { border-top: 1px solid var(--background-border-color); } diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index f551f69db8..142a05d10d 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -83,6 +83,6 @@ class PermalinkRedirector end def path_segments - @path_segments ||= @path.delete_prefix('/deck').delete_prefix('/').split('/') + @path_segments ||= @path.split('?')[0].delete_prefix('/deck').delete_prefix('/').split('/') end end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 6b21b4bedd..4c374f5d57 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -13,12 +13,14 @@ class NotificationMailer < ApplicationMailer before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request] after_action :set_list_headers! + before_deliver :verify_functional_user + default to: -> { email_address_with_name(@user.email, @me.username) } layout 'mailer' def mention - return unless @user.functional? && @status.present? + return if @status.blank? locale_for_account(@me) do mail subject: default_i18n_subject(name: @status.account.acct) @@ -26,15 +28,13 @@ class NotificationMailer < ApplicationMailer end def follow - return unless @user.functional? - locale_for_account(@me) do mail subject: default_i18n_subject(name: @account.acct) end end def favourite - return unless @user.functional? && @status.present? + return if @status.blank? locale_for_account(@me) do mail subject: default_i18n_subject(name: @account.acct) @@ -42,7 +42,7 @@ class NotificationMailer < ApplicationMailer end def reblog - return unless @user.functional? && @status.present? + return if @status.blank? locale_for_account(@me) do mail subject: default_i18n_subject(name: @account.acct) @@ -50,8 +50,6 @@ class NotificationMailer < ApplicationMailer end def follow_request - return unless @user.functional? - locale_for_account(@me) do mail subject: default_i18n_subject(name: @account.acct) end @@ -75,6 +73,10 @@ class NotificationMailer < ApplicationMailer @account = @notification.from_account end + def verify_functional_user + throw(:abort) unless @user.functional? + end + def set_list_headers! headers( 'List-ID' => "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>", diff --git a/app/models/notification.rb b/app/models/notification.rb index 44a43d2ece..695f39a316 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -20,6 +20,7 @@ class Notification < ApplicationRecord self.inheritance_column = nil include Paginable + include Redisable LEGACY_TYPE_CLASS_MAP = { 'Mention' => :mention, @@ -30,7 +31,9 @@ class Notification < ApplicationRecord 'Poll' => :poll, }.freeze - GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog).freeze + # `set_group_key!` needs to be updated if this list changes + GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow).freeze + MAXIMUM_GROUP_SPAN_HOURS = 12 # Please update app/javascript/api_types/notification.ts if you change this PROPERTIES = { @@ -123,6 +126,30 @@ class Notification < ApplicationRecord end end + def set_group_key! + return if filtered? || Notification::GROUPABLE_NOTIFICATION_TYPES.exclude?(type) + + type_prefix = case type + when :favourite, :reblog + [type, target_status&.id].join('-') + when :follow + type + else + raise NotImplementedError + end + redis_key = "notif-group/#{account.id}/#{type_prefix}" + hour_bucket = activity.created_at.utc.to_i / 1.hour.to_i + + # Reuse previous group if it does not span too large an amount of time + previous_bucket = redis.get(redis_key).to_i + hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS + + # We do not concern ourselves with race conditions since we use hour buckets + redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i) + + self.group_key = "#{type_prefix}-#{hour_bucket}" + end + class << self def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false) requested_types = if types.empty? diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 97eee05487..9aebab787e 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -3,8 +3,6 @@ class NotifyService < BaseService include Redisable - MAXIMUM_GROUP_SPAN_HOURS = 12 - # TODO: the severed_relationships type probably warrants email notifications NON_EMAIL_TYPES = %i( admin.report @@ -216,7 +214,7 @@ class NotifyService < BaseService return if drop? @notification.filtered = filter? - @notification.group_key = notification_group_key + @notification.set_group_key! @notification.save! # It's possible the underlying activity has been deleted @@ -236,23 +234,6 @@ class NotifyService < BaseService private - def notification_group_key - return nil if @notification.filtered || Notification::GROUPABLE_NOTIFICATION_TYPES.exclude?(@notification.type) - - type_prefix = "#{@notification.type}-#{@notification.target_status.id}" - redis_key = "notif-group/#{@recipient.id}/#{type_prefix}" - hour_bucket = @notification.activity.created_at.utc.to_i / 1.hour.to_i - - # Reuse previous group if it does not span too large an amount of time - previous_bucket = redis.get(redis_key).to_i - hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS - - # We do not concern ourselves with race conditions since we use hour buckets - redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i) - - "#{type_prefix}-#{hour_bucket}" - end - def drop? DropCondition.new(@notification).drop? end diff --git a/config/locales/activerecord.eo.yml b/config/locales/activerecord.eo.yml index 99059e3856..f99f726e23 100644 --- a/config/locales/activerecord.eo.yml +++ b/config/locales/activerecord.eo.yml @@ -15,6 +15,12 @@ eo: user/invite_request: text: Kialo errors: + attributes: + domain: + invalid: ne estas valida domajna nomo + messages: + invalid_domain_on_line: "%{value} ne estas valida domajna nomo" + too_many_lines: superas la limon de %{limit} linioj models: account: attributes: diff --git a/config/locales/devise.eo.yml b/config/locales/devise.eo.yml index 193fecc757..f0322a60a8 100644 --- a/config/locales/devise.eo.yml +++ b/config/locales/devise.eo.yml @@ -48,10 +48,13 @@ eo: subject: 'Mastodon: Instrukcioj por ŝanĝi pasvorton' title: Pasvorto restarigita two_factor_disabled: + explanation: Ensalutu nun eblas uzante nur retadreson kaj pasvorton. subject: 'Mastodon: dufaktora aŭtentigo malebligita' + subtitle: Dupaŝa aŭtentigo por via konto estas malŝaltita. title: 2FA estas malŝaltita two_factor_enabled: subject: 'Mastodon: Dufaktora aŭtentigo ebligita' + subtitle: Dupaŝa aŭtentigo por via konto estas ŝaltita. title: 2FA aktivigita two_factor_recovery_codes_changed: explanation: La antaŭaj reakiraj kodoj estis nuligitaj kaj novaj estis generitaj. diff --git a/config/locales/doorkeeper.es-AR.yml b/config/locales/doorkeeper.es-AR.yml index 9f3c862272..804e4a51ed 100644 --- a/config/locales/doorkeeper.es-AR.yml +++ b/config/locales/doorkeeper.es-AR.yml @@ -60,7 +60,7 @@ es-AR: error: title: Ocurrió un error new: - prompt_html: "%{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente." + prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente. review_permissions: Revisar permisos title: Autorización requerida show: diff --git a/config/locales/doorkeeper.es-MX.yml b/config/locales/doorkeeper.es-MX.yml index 419bb58f95..c095777954 100644 --- a/config/locales/doorkeeper.es-MX.yml +++ b/config/locales/doorkeeper.es-MX.yml @@ -60,7 +60,7 @@ es-MX: error: title: Ha ocurrido un error new: - prompt_html: "%{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente." + prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente. review_permissions: Revisar permisos title: Se requiere autorización show: diff --git a/config/locales/doorkeeper.es.yml b/config/locales/doorkeeper.es.yml index 093d84397a..c26f11a7a1 100644 --- a/config/locales/doorkeeper.es.yml +++ b/config/locales/doorkeeper.es.yml @@ -60,7 +60,7 @@ es: error: title: Ha ocurrido un error new: - prompt_html: "%{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente." + prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. Solo aprueba esta solicitud si reconoces y confías en esta fuente. review_permissions: Revisar permisos title: Se requiere autorización show: diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml index ff37786d28..a13c362173 100644 --- a/config/locales/doorkeeper.hu.yml +++ b/config/locales/doorkeeper.hu.yml @@ -60,6 +60,7 @@ hu: error: title: Hiba történt new: + prompt_html: A(z) %{client_name} engedélyt kér hogy hozzáférjen a fiókodhoz. Csak akkor engedélyezd ezt a kérést, ha felismered és megbízol ebben a forrásban. review_permissions: Jogosultságok áttekintése title: Engedélyezés szükséges show: diff --git a/config/locales/doorkeeper.vi.yml b/config/locales/doorkeeper.vi.yml index 195e527f70..6687c0339d 100644 --- a/config/locales/doorkeeper.vi.yml +++ b/config/locales/doorkeeper.vi.yml @@ -71,7 +71,7 @@ vi: confirmations: revoke: Bạn có chắc không? index: - authorized_at: Cho phép %{date} + authorized_at: Cho phép vào %{date} description_html: Đây là những ứng dụng có thể truy cập tài khoản của bạn bằng API. Nếu có ứng dụng bạn không nhận ra ở đây hoặc ứng dụng hoạt động sai, bạn có thể thu hồi quyền truy cập của ứng dụng đó. last_used_at: Dùng lần cuối %{date} never_used: Chưa dùng @@ -151,7 +151,7 @@ vi: scopes: admin:read: đọc mọi dữ liệu trên máy chủ admin:read:accounts: đọc thông tin nhạy cảm của tất cả các tài khoản - admin:read:canonical_email_blocks: đọc thông tin nhạy cảm của tất cả các khối email chuẩn + admin:read:canonical_email_blocks: đọc thông tin nhạy cảm của tất cả khối email chuẩn admin:read:domain_allows: đọc thông tin nhạy cảm của tất cả các tên miền cho phép admin:read:domain_blocks: đọc thông tin nhạy cảm của tất cả các tên miền chặn admin:read:email_domain_blocks: đọc thông tin nhạy cảm của tất cả các miền email chặn diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index e675209017..010bb262ad 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -42,7 +42,7 @@ vi: autofollow: Những người đăng ký sẽ tự động theo dõi bạn avatar: WEBP, PNG, GIF hoặc JPG, tối đa %{size}. Sẽ bị nén xuống %{dimensions}px bot: Tài khoản này tự động thực hiện các hành động và không được quản lý bởi người thật - context: Chọn một hoặc nhiều nơi mà bộ lọc sẽ áp dụng + context: Chọn những nơi mà bộ lọc sẽ áp dụng current_password: Vì mục đích bảo mật, vui lòng nhập mật khẩu của tài khoản hiện tại current_username: Để xác nhận, vui lòng nhập tên người dùng của tài khoản hiện tại digest: Chỉ gửi sau một thời gian dài không hoạt động hoặc khi bạn nhận được tin nhắn (trong thời gian vắng mặt) @@ -51,7 +51,7 @@ vi: inbox_url: Sao chép URL của máy chủ mà bạn muốn dùng irreversible: Các tút đã lọc sẽ không thể phục hồi, kể cả sau khi xóa bộ lọc locale: Ngôn ngữ của giao diện, email và thông báo đẩy - password: Dùng ít nhất 8 ký tự + password: Tối thiểu 8 ký tự phrase: Sẽ được hiện thị trong văn bản hoặc cảnh báo nội dung của một tút scopes: Ứng dụng sẽ được phép truy cập những API nào. Nếu bạn chọn quyền cấp cao nhất, không cần chọn quyền nhỏ. setting_aggregate_reblogs: Nếu một tút đã được đăng lại thì những lượt đăng lại sau sẽ không hiện trên bảng tin nữa @@ -74,8 +74,8 @@ vi: filters: action: Chọn hành động sẽ thực hiện khi một tút khớp với bộ lọc actions: - hide: Ẩn hoàn toàn nội dung đã lọc, như thể nó không tồn tại - warn: Ẩn nội dung đã lọc đằng sau một cảnh báo đề cập đến tiêu đề của bộ lọc + hide: Ẩn hoàn toàn, như thể nó không tồn tại + warn: Hiện cảnh báo và bộ lọc form_admin_settings: activity_api_enabled: Số lượng tút được đăng trong máy chủ, người dùng đang hoạt động và đăng ký mới hàng tuần app_icon: WEBP, PNG, GIF hoặc JPG. Dùng biểu tượng tùy chỉnh trên thiết bị di động. @@ -226,7 +226,7 @@ vi: setting_theme: Giao diện setting_trends: Hiển thị xu hướng trong ngày setting_unfollow_modal: Hỏi trước khi bỏ theo dõi ai đó - setting_use_blurhash: Phủ màu media nhạy cảm + setting_use_blurhash: Làm mờ media nhạy cảm setting_use_pending_items: Không tự động cập nhật bảng tin severity: Mức độ nghiêm trọng sign_in_token_attempt: Mã an toàn @@ -305,7 +305,7 @@ vi: label: Đã có phiên bản Mastodon mới none: Không bao giờ thông báo (không đề xuất) patch: Thông báo bản cập sửa lỗi - trending_tag: Phê duyệt nội dung nổi bật mới + trending_tag: Phê duyệt xu hướng mới rule: hint: Thông tin thêm text: Nội quy diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 83531c6561..d969ad7d4f 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -44,7 +44,7 @@ vi: submit: Thay đổi email title: Thay đổi email cho %{username} change_role: - changed_msg: Vai trò đã thay đổi thành công! + changed_msg: Đã cập nhật vai trò! edit_roles: Quản lý vai trò người dùng label: Đổi vai trò no_role: Chưa có vai trò @@ -55,7 +55,7 @@ vi: custom: Tùy chỉnh delete: Xóa dữ liệu deleted: Đã xóa - demote: Xóa vai trò + demote: Hạ vai trò destroyed_msg: Dữ liệu %{username} sẽ được lên lịch xóa ngay bây giờ disable: Khóa disable_sign_in_token_auth: Tắt xác minh bằng email @@ -108,7 +108,7 @@ vi: previous_strikes: Lịch sử kiểm duyệt previous_strikes_description_html: other: Người này bị cảnh cáo %{count} lần. - promote: Chỉ định vai trò + promote: Nâng vai trò protocol: Giao thức public: Công khai push_subscription_expires: Đăng ký PuSH hết hạn @@ -153,8 +153,8 @@ vi: suspension_irreversible: Toàn bộ dữ liệu của người này sẽ bị xóa hết. Bạn vẫn có thể ngừng vô hiệu hóa nhưng dữ liệu sẽ không thể phục hồi. suspension_reversible_hint_html: Mọi dữ liệu của người này sẽ bị xóa sạch vào %{date}. Trước thời hạn này, dữ liệu vẫn có thể phục hồi. Nếu bạn muốn xóa dữ liệu của người này ngay lập tức, hãy tiếp tục. title: Tài khoản - unblock_email: Mở khóa địa chỉ email - unblocked_email_msg: Mở khóa thành công địa chỉ email của %{username} + unblock_email: Bỏ chặn địa chỉ email + unblocked_email_msg: Đã bỏ chặn địa chỉ email của %{username} unconfirmed_email: Email chưa được xác minh undo_sensitized: Đánh dấu bình thường undo_silenced: Bỏ hạn chế @@ -170,42 +170,42 @@ vi: action_logs: action_types: approve_appeal: Chấp nhận kháng cáo - approve_user: Chấp nhận đăng ký + approve_user: Duyệt đăng ký assigned_to_self_report: Tự xử lý báo cáo change_email_user: Đổi email người dùng change_role_user: Đổi vai trò confirm_user: Xác minh create_account_warning: Cảnh cáo create_announcement: Tạo thông báo mới - create_canonical_email_block: Tạo chặn email + create_canonical_email_block: Chặn địa chỉ email create_custom_emoji: Tạo emoji create_domain_allow: Cho phép máy chủ create_domain_block: Chặn máy chủ create_email_domain_block: Tạo chặn tên miền email - create_ip_block: Tạo chặn IP mới - create_unavailable_domain: Máy chủ không khả dụng + create_ip_block: Chặn IP + create_unavailable_domain: Ngừng liên hợp create_user_role: Tạo vai trò - demote_user: Xóa vai trò + demote_user: Hạ vai trò destroy_announcement: Xóa thông báo - destroy_canonical_email_block: Bỏ chặn email + destroy_canonical_email_block: Bỏ chặn địa chỉ email destroy_custom_emoji: Xóa emoji destroy_domain_allow: Bỏ thanh trừng máy chủ destroy_domain_block: Bỏ chặn máy chủ destroy_email_domain_block: Bỏ chặn tên miền email destroy_instance: Thanh trừng máy chủ - destroy_ip_block: Xóa IP đã chặn + destroy_ip_block: Bỏ chặn IP destroy_status: Xóa tút - destroy_unavailable_domain: Xóa máy chủ không khả dụng + destroy_unavailable_domain: Tái liên hợp destroy_user_role: Xóa vai trò disable_2fa_user: Vô hiệu hóa 2FA disable_custom_emoji: Vô hiệu hóa emoji disable_sign_in_token_auth_user: Tắt xác minh bằng email cho người dùng disable_user: Vô hiệu hóa đăng nhập - enable_custom_emoji: Cho phép emoji + enable_custom_emoji: Duyệt emoji enable_sign_in_token_auth_user: Bật xác minh bằng email cho người dùng - enable_user: Bỏ vô hiệu hóa đăng nhập + enable_user: Cho phép đăng nhập memorialize_account: Đánh dấu tưởng niệm - promote_user: Chỉ định vai trò + promote_user: Nâng vai trò reject_appeal: Từ chối kháng cáo reject_user: Từ chối đăng ký remove_avatar_user: Xóa ảnh đại diện @@ -213,11 +213,11 @@ vi: resend_user: Gửi lại email xác minh reset_password_user: Đặt lại mật khẩu resolve_report: Xử lý báo cáo - sensitive_account: Áp đặt nhạy cảm - silence_account: Áp đặt ẩn - suspend_account: Áp đặt vô hiệu hóa + sensitive_account: Gán nhạy cảm + silence_account: Gán ẩn + suspend_account: Gán vô hiệu hóa unassigned_report: Báo cáo chưa xử lý - unblock_email_account: Mở khóa địa chỉ email + unblock_email_account: Bỏ chặn địa chỉ email unsensitive_account: Bỏ nhạy cảm unsilence_account: Bỏ ẩn unsuspend_account: Bỏ vô hiệu hóa @@ -229,7 +229,7 @@ vi: update_status: Cập nhật tút update_user_role: Cập nhật vai trò actions: - approve_appeal_html: "%{name} đã chấp nhận kháng cáo của %{target}" + approve_appeal_html: "%{name} đã duyệt kháng cáo của %{target}" approve_user_html: "%{name} đã chấp nhận đăng ký từ %{target}" assigned_to_self_report_html: "%{name} tự xử lý báo cáo %{target}" change_email_user_html: "%{name} đã thay đổi địa chỉ email của %{target}" @@ -237,7 +237,7 @@ vi: confirm_user_html: "%{name} đã xác minh địa chỉ email của %{target}" create_account_warning_html: "%{name} đã cảnh cáo %{target}" create_announcement_html: "%{name} tạo thông báo mới %{target}" - create_canonical_email_block_html: "%{name} đã chặn email với hash %{target}" + create_canonical_email_block_html: "%{name} đã chặn địa chỉ email với hash %{target}" create_custom_emoji_html: "%{name} đã tải lên biểu tượng cảm xúc mới %{target}" create_domain_allow_html: "%{name} kích hoạt liên hợp với %{target}" create_domain_block_html: "%{name} chặn máy chủ %{target}" @@ -245,9 +245,9 @@ vi: create_ip_block_html: "%{name} đã chặn IP %{target}" create_unavailable_domain_html: "%{name} ngưng phân phối với máy chủ %{target}" create_user_role_html: "%{name} đã tạo vai trò %{target}" - demote_user_html: "%{name} đã xóa vai trò của %{target}" + demote_user_html: "%{name} đã hạ vai trò của %{target}" destroy_announcement_html: "%{name} xóa thông báo %{target}" - destroy_canonical_email_block_html: "%{name} đã bỏ chặn email với hash %{target}" + destroy_canonical_email_block_html: "%{name} đã bỏ chặn địa chỉ email với hash %{target}" destroy_custom_emoji_html: "%{name} đã xóa emoji %{target}" destroy_domain_allow_html: "%{name} đã ngừng liên hợp với %{target}" destroy_domain_block_html: "%{name} bỏ chặn máy chủ %{target}" @@ -261,11 +261,11 @@ vi: disable_custom_emoji_html: "%{name} đã ẩn emoji %{target}" disable_sign_in_token_auth_user_html: "%{name} đã tắt xác minh email của %{target}" disable_user_html: "%{name} vô hiệu hóa đăng nhập %{target}" - enable_custom_emoji_html: "%{name} cho phép Emoji %{target}" + enable_custom_emoji_html: "%{name} cho phép emoji %{target}" enable_sign_in_token_auth_user_html: "%{name} đã bật xác minh email của %{target}" enable_user_html: "%{name} bỏ vô hiệu hóa đăng nhập %{target}" memorialize_account_html: "%{name} đã biến tài khoản %{target} thành một trang tưởng niệm" - promote_user_html: "%{name} chỉ định vai trò cho %{target}" + promote_user_html: "%{name} đã nâng vai trò của %{target}" reject_appeal_html: "%{name} đã từ chối kháng cáo của %{target}" reject_user_html: "%{name} đã từ chối đăng ký từ %{target}" remove_avatar_user_html: "%{name} đã xóa ảnh đại diện của %{target}" @@ -277,7 +277,7 @@ vi: silence_account_html: "%{name} đã ẩn %{target}" suspend_account_html: "%{name} đã vô hiệu hóa %{target}" unassigned_report_html: "%{name} đã xử lý báo cáo %{target} chưa xử lí" - unblock_email_account_html: "%{name} mở khóa địa chỉ email của %{target}" + unblock_email_account_html: "%{name} bỏ chặn địa chỉ email của %{target}" unsensitive_account_html: "%{name} đánh dấu nội dung của %{target} là bình thường" unsilence_account_html: "%{name} đã bỏ ẩn %{target}" unsuspend_account_html: "%{name} đã bỏ vô hiệu hóa %{target}" @@ -287,7 +287,7 @@ vi: update_ip_block_html: "%{name} cập nhật chặn IP %{target}" update_report_html: "%{name} cập nhật báo cáo %{target}" update_status_html: "%{name} cập nhật tút của %{target}" - update_user_role_html: "%{name} đã thay đổi vai trò %{target}" + update_user_role_html: "%{name} đã cập nhật vai trò %{target}" deleted_account: tài khoản đã xóa empty: Không tìm thấy bản ghi. filter_by_action: Theo hành động @@ -328,7 +328,7 @@ vi: emoji: Emoji enable: Cho phép enabled: Đã cho phép - enabled_msg: Đã cho phép thành công Emoji này + enabled_msg: Đã cho phép emoji này xong image_hint: PNG hoặc GIF tối đa %{size} list: Danh sách listed: Liệt kê @@ -692,7 +692,7 @@ vi: manage_announcements: Quản lý thông báo manage_announcements_description: Cho phép quản lý thông báo trên máy chủ manage_appeals: Quản lý kháng cáo - manage_appeals_description: Cho phép xem xét kháng cáo đối với các hành động kiểm duyệt + manage_appeals_description: Cho phép thành viên kháng cáo đối với các hành động kiểm duyệt manage_blocks: Quản lý chặn manage_blocks_description: Cho phép người dùng tự chặn các nhà cung cấp email và địa chỉ IP manage_custom_emojis: Quản lý emoji @@ -704,7 +704,7 @@ vi: manage_reports: Quản lý báo cáo manage_reports_description: Cho phép xem xét các báo cáo và thực hiện hành động kiểm duyệt đối với chúng manage_roles: Quản lý vai trò - manage_roles_description: Cho phép quản lý và chỉ định các vai trò nhỏ hơn họ + manage_roles_description: Cho phép quản lý và nâng cấp các vai trò nhỏ hơn họ manage_rules: Quản lý nội quy máy chủ manage_rules_description: Cho phép thay đổi nội quy máy chủ manage_settings: Quản lý thiết lập @@ -798,7 +798,7 @@ vi: patch: Bản vá - sửa lỗi và dễ dàng áp dụng các thay đổi version: Phiên bản statuses: - account: Tác giả + account: Người đăng application: Ứng dụng back_to_account: Quay lại trang tài khoản back_to_report: Quay lại trang báo cáo @@ -817,7 +817,7 @@ vi: open: Mở tút original_status: Tút gốc reblogs: Lượt đăng lại - status_changed: Tút đã thay đổi + status_changed: Tút đã sửa title: Toàn bộ tút trending: Xu hướng visibility: Hiển thị @@ -896,7 +896,7 @@ vi: title: Quản trị trends: allow: Cho phép - approved: Đã cho phép + approved: Đã duyệt confirm_allow: Bạn có chắc muốn cho phép những hashtag đã chọn? confirm_disallow: Bạn có chắc muốn cấm những hashtag đã chọn? disallow: Cấm @@ -915,17 +915,17 @@ vi: no_publisher_selected: Không có nguồn đăng nào thay đổi vì không có nguồn đăng nào được chọn shared_by_over_week: other: "%{count} người chia sẻ tuần rồi" - title: Tin tức nổi bật + title: Xu hướng tin tức usage_comparison: Chia sẻ %{today} lần hôm nay, so với %{yesterday} lần hôm qua not_allowed_to_trend: Không được phép thành xu hướng - only_allowed: Chỉ cho phép + only_allowed: Đã cho phép pending_review: Đang chờ preview_card_providers: allowed: Tin tức từ nguồn này có thể lên xu hướng description_html: Đây là những nguồn mà từ đó các liên kết thường được chia sẻ trên máy chủ của bạn. Các liên kết sẽ không thể lên xu hướng trừ khi bạn cho phép nguồn. Sự cho phép (hoặc cấm) của bạn áp dụng luôn cho các tên miền phụ. rejected: Tin tức từ nguồn này không thể lên xu hướng title: Nguồn đăng - rejected: Đã cấm + rejected: Từ chối statuses: allow: Cho phép tút allow_account: Cho phép người đăng @@ -936,11 +936,11 @@ vi: description_html: Đây là những tút đang được chia sẻ và yêu thích rất nhiều trên máy chủ của bạn. Nó có thể giúp người mới và người cũ tìm thấy nhiều người hơn để theo dõi. Không có tút nào được hiển thị công khai cho đến khi bạn cho phép người đăng và người cho phép đề xuất tài khoản của họ cho người khác. Bạn cũng có thể cho phép hoặc từ chối từng tút riêng. disallow: Cấm tút disallow_account: Cấm người đăng - no_status_selected: Không có tút xu hướng nào thay đổi vì không có tút nào được chọn - not_discoverable: Tác giả đã chọn không tham gia mục khám phá + no_status_selected: Bạn chưa chọn mục nào + not_discoverable: Người đăng đã chọn không tham gia mục khám phá shared_by: other: Được thích và đăng lại %{friendly_count} lần - title: Tút xu hướng + title: Xu hướng tút tags: current_score: Chỉ số gần đây %{score} dashboard: @@ -956,9 +956,9 @@ vi: not_trendable: Không cho lên xu hướng not_usable: Không được phép dùng peaked_on_and_decaying: Đỉnh điểm %{date}, giờ đang giảm - title: Hashtag nổi bật + title: Xu hướng hashtag trendable: Cho phép lên xu hướng - trending_rank: 'Nổi bật #%{rank}' + trending_rank: 'Xu hướng #%{rank}' usable: Có thể dùng usage_comparison: Dùng %{today} lần hôm nay, so với %{yesterday} hôm qua used_by_over_week: @@ -1004,7 +1004,7 @@ vi: silence: hạn chế tài khoản của họ suspend: vô hiệu hóa tài khoản của họ body: "%{target} đã khiếu nại vì bị %{action_taken_by} %{type} vào %{date}. Họ cho biết:" - next_steps: Bạn có thể chấp nhận kháng cáo để hủy kiểm duyệt hoặc bỏ qua. + next_steps: Bạn có thể duyệt kháng cáo để hủy kiểm duyệt hoặc bỏ qua. subject: "%{username} đang khiếu nại quyết định kiểm duyệt trên %{instance}" new_critical_software_updates: body: Các phiên bản quan trọng mới của Mastodon đã được phát hành, bạn nên cập nhật càng sớm càng tốt! @@ -1022,12 +1022,12 @@ vi: new_trends: body: 'Các mục sau đây cần được xem xét trước khi chúng hiển thị công khai:' new_trending_links: - title: Tin tức nổi bật + title: Xu hướng tin tức new_trending_statuses: - title: Tút nổi bật + title: Xu hướng tút new_trending_tags: - title: Hashtag nổi bật - subject: Nội dung nổi bật chờ duyệt trên %{instance} + title: Xu hướng hashtag + subject: Xu hướng chờ duyệt trên %{instance} aliases: add_new: Kết nối tài khoản created_msg: Tạo thành công một tên hiển thị mới. Bây giờ bạn có thể bắt đầu di chuyển từ tài khoản cũ. @@ -1147,7 +1147,7 @@ vi: hint_html: Kiểm soát cách bạn được ghi nhận khi chia sẻ liên kết trên Mastodon. more_from_html: Thêm từ %{name} s_blog: "%{name}'s Blog" - title: Ghi nhận tác giả + title: Ghi nhận người đăng challenge: confirm: Tiếp tục hint_html: "Mẹo: Chúng tôi sẽ không hỏi lại mật khẩu của bạn sau này." @@ -1201,7 +1201,7 @@ vi: appealed_msg: Khiếu nại của bạn đã được gửi đi. Nếu nó được chấp nhận, bạn sẽ nhận được thông báo. appeals: submit: Gửi khiếu nại - approve_appeal: Chấp nhận kháng cáo + approve_appeal: Duyệt kháng cáo associated_report: Báo cáo đính kèm created_at: Ngày description_html: Đây là những cảnh cáo và áp đặt kiểm duyệt đối với bạn bởi đội ngũ %{instance}. @@ -1280,7 +1280,7 @@ vi: deprecated_api_multiple_keywords: Không thể thay đổi các tham số này từ ứng dụng này vì chúng áp dụng cho nhiều hơn một từ khóa bộ lọc. Sử dụng ứng dụng mới hơn hoặc giao diện web. invalid_context: Bối cảnh không hợp lệ hoặc không có index: - contexts: Bộ lọc %{contexts} + contexts: Lọc ở %{contexts} delete: Xóa bỏ empty: Chưa có bộ lọc nào. expires_in: Hết hạn trong %{distance} @@ -1336,7 +1336,7 @@ vi: merge: Hợp nhất merge_long: Giữ hồ sơ hiện có và thêm hồ sơ mới overwrite: Ghi đè - overwrite_long: Thay thế các bản ghi hiện tại bằng những cái mới + overwrite_long: Thay thế các bản ghi hiện tại bằng các bản ghi mới overwrite_preambles: blocking_html: Bạn sắp thay thế danh sách chặn với %{total_items} tài khoản từ %{filename}. bookmarks_html: Bạn sắp thay thế lượt lưu với %{total_items} tút từ %{filename}. @@ -1414,7 +1414,7 @@ vi: description_html: Nếu có lần đăng nhập đáng ngờ, hãy đổi ngay mật khẩu và bật xác minh 2 bước. empty: Không có lịch sử đăng nhập failed_sign_in_html: Đăng nhập thất bại bằng %{method} từ %{ip} (%{browser}) - successful_sign_in_html: Đăng nhập thành công bằng %{method} từ %{ip} (%{browser}) + successful_sign_in_html: Đăng nhập bằng %{method} từ %{ip} (%{browser}) title: Lịch sử đăng nhập mail_subscriptions: unsubscribe: @@ -1832,14 +1832,14 @@ vi: spam: Spam violation: Nội dung vi phạm quy tắc cộng đồng explanation: - delete_statuses: Vài tút của bạn đã vi phạm nội quy máy chủ và tạm thời bị ẩn bởi kiểm duyệt viên của %{instance}. + delete_statuses: Tút của bạn đã vi phạm nội quy máy chủ và tạm thời bị ẩn bởi kiểm duyệt viên của %{instance}. disable: Bạn không còn có thể sử dụng tài khoản của mình, nhưng hồ sơ của bạn và dữ liệu khác vẫn còn nguyên. Bạn có thể yêu cầu sao lưu dữ liệu của mình, thay đổi cài đặt tài khoản hoặc xóa tài khoản của bạn. mark_statuses_as_sensitive: Vài tút của bạn đã bị kiểm duyệt viên %{instance} đánh dấu nhạy cảm. Mọi người cần nhấn vào media để xem nó. Bạn có thể tự đánh dấu tài khoản của bạn là nhạy cảm. sensitive: Từ giờ trở đi, tất cả các media của bạn bạn tải lên sẽ được đánh dấu là nhạy cảm và ẩn đằng sau cảnh báo nhấp chuột. silence: Bạn vẫn có thể sử dụng tài khoản của mình, nhưng chỉ những người đang theo dõi bạn mới thấy bài đăng của bạn. Bạn cũng bị loại khỏi các tính năng khám phá khác. Tuy nhiên, những người khác vẫn có thể theo dõi bạn. suspend: Bạn không còn có thể sử dụng tài khoản của bạn, hồ sơ và các dữ liệu khác không còn có thể truy cập được. Trong vòng 30 ngày, bạn vẫn có thể đăng nhập để yêu cầu bản sao dữ liệu của mình cho đến khi dữ liệu bị xóa hoàn toàn, nhưng chúng tôi sẽ giữ lại một số dữ liệu cơ bản để ngăn bạn thoát khỏi việc vô hiệu hóa. reason: 'Lý do:' - statuses: 'Tút lưu ý:' + statuses: 'Tút vi phạm:' subject: delete_statuses: Những tút %{acct} của bạn đã bị xóa bỏ disable: Tài khoản %{acct} của bạn đã bị vô hiệu hóa diff --git a/config/routes/api.rb b/config/routes/api.rb index 48b35cc154..a5f49d9f43 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -301,21 +301,6 @@ namespace :api, format: false do end end - concern :grouped_notifications do - resources :notifications, param: :group_key, only: [:index, :show] do - collection do - post :clear - get :unread_count - end - - member do - post :dismiss - end - - resources :accounts, only: [:index], module: :notifications - end - end - namespace :v2 do get '/search', to: 'search#index', as: :search @@ -342,11 +327,18 @@ namespace :api, format: false do resource :policy, only: [:show, :update] end - concerns :grouped_notifications - end + resources :notifications, param: :group_key, only: [:index, :show] do + collection do + post :clear + get :unread_count + end - namespace :v2_alpha, module: 'v2' do - concerns :grouped_notifications + member do + post :dismiss + end + + resources :accounts, only: [:index], module: :notifications + end end namespace :web do diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 9bb520211c..cfc80b8650 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -10,13 +10,6 @@ RSpec.describe Oauth::AuthorizationsController do get :new, params: { client_id: app.uid, response_type: 'code', redirect_uri: 'http://localhost/', scope: 'read' } end - shared_examples 'stores location for user' do - it 'stores location for user' do - subject - expect(controller.stored_location_for(:user)).to eq "/oauth/authorize?client_id=#{app.uid}&redirect_uri=http%3A%2F%2Flocalhost%2F&response_type=code&scope=read" - end - end - context 'when signed in' do let!(:user) { Fabricate(:user) } @@ -24,18 +17,17 @@ RSpec.describe Oauth::AuthorizationsController do sign_in user, scope: :user end - it 'returns http success' do + it 'returns http success and private cache control headers' do subject - expect(response).to have_http_status(200) - end - it 'returns private cache control headers' do - subject - expect(response.headers['Cache-Control']).to include('private, no-store') + expect(response) + .to have_http_status(200) + expect(response.headers['Cache-Control']) + .to include('private, no-store') + expect(controller.stored_location_for(:user)) + .to eq authorize_path_for(app) end - include_examples 'stores location for user' - context 'when app is already authorized' do before do Doorkeeper::AccessToken.find_or_create_for( @@ -52,10 +44,12 @@ RSpec.describe Oauth::AuthorizationsController do expect(response).to redirect_to(/\A#{app.redirect_uri}/) end - it 'does not redirect to callback with force_login=true' do - get :new, params: { client_id: app.uid, response_type: 'code', redirect_uri: 'http://localhost/', scope: 'read', force_login: 'true' } + context 'with `force_login` param true' do + subject do + get :new, params: { client_id: app.uid, response_type: 'code', redirect_uri: 'http://localhost/', scope: 'read', force_login: 'true' } + end - expect(response).to have_http_status(:success) + it { is_expected.to have_http_status(:success) } end end end @@ -63,10 +57,16 @@ RSpec.describe Oauth::AuthorizationsController do context 'when not signed in' do it 'redirects' do subject - expect(response).to redirect_to '/auth/sign_in' - end - include_examples 'stores location for user' + expect(response) + .to redirect_to '/auth/sign_in' + expect(controller.stored_location_for(:user)) + .to eq authorize_path_for(app) + end + end + + def authorize_path_for(app) + "/oauth/authorize?client_id=#{app.uid}&redirect_uri=http%3A%2F%2Flocalhost%2F&response_type=code&scope=read" end end end diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb index 52d3dbde83..1cf0984abe 100644 --- a/spec/controllers/oauth/authorized_applications_controller_spec.rb +++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb @@ -10,38 +10,31 @@ RSpec.describe Oauth::AuthorizedApplicationsController do get :index end - shared_examples 'stores location for user' do - it 'stores location for user' do - subject - expect(controller.stored_location_for(:user)).to eq '/oauth/authorized_applications' - end - end - context 'when signed in' do before do sign_in Fabricate(:user), scope: :user end - it 'returns http success' do + it 'returns http success with private cache control headers' do subject - expect(response).to have_http_status(200) + expect(response) + .to have_http_status(200) + expect(response.headers['Cache-Control']) + .to include('private, no-store') + expect(controller.stored_location_for(:user)) + .to eq '/oauth/authorized_applications' end - - it 'returns private cache control headers' do - subject - expect(response.headers['Cache-Control']).to include('private, no-store') - end - - include_examples 'stores location for user' end context 'when not signed in' do it 'redirects' do subject - expect(response).to redirect_to '/auth/sign_in' - end - include_examples 'stores location for user' + expect(response) + .to redirect_to '/auth/sign_in' + expect(controller.stored_location_for(:user)) + .to eq '/oauth/authorized_applications' + end end end @@ -55,23 +48,19 @@ RSpec.describe Oauth::AuthorizedApplicationsController do before do sign_in user, scope: :user allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub) + end + + it 'revokes access tokens for the application and removes subscriptions and sends kill payload to streaming' do post :destroy, params: { id: application.id } - end - it 'revokes access tokens for the application' do - expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at).to_not be_nil - end - - it 'removes subscriptions for the application\'s access tokens' do - expect(Web::PushSubscription.where(user: user).count).to eq 0 - end - - it 'removes the web_push_subscription' do - expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'sends a session kill payload to the streaming server' do - expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}') + expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at) + .to_not be_nil + expect(Web::PushSubscription.where(user: user).count) + .to eq(0) + expect { web_push_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) + expect(redis_pipeline_stub) + .to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}') end end end diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb index dd2d8ca70c..a2eed797e0 100644 --- a/spec/controllers/oauth/tokens_controller_spec.rb +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -9,20 +9,15 @@ RSpec.describe Oauth::TokensController do let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } - before do + it 'revokes the token and removes subscriptions' do post :revoke, params: { client_id: application.uid, token: access_token.token } - end - it 'revokes the token' do - expect(access_token.reload.revoked_at).to_not be_nil - end - - it 'removes web push subscription for token' do - expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0 - end - - it 'removes the web_push_subscription' do - expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(access_token.reload.revoked_at) + .to_not be_nil + expect(Web::PushSubscription.where(access_token: access_token).count) + .to eq(0) + expect { web_push_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/settings/featured_tags_controller_spec.rb b/spec/controllers/settings/featured_tags_controller_spec.rb index a56ae1c498..f414e818f5 100644 --- a/spec/controllers/settings/featured_tags_controller_spec.rb +++ b/spec/controllers/settings/featured_tags_controller_spec.rb @@ -5,16 +5,10 @@ require 'rails_helper' RSpec.describe Settings::FeaturedTagsController do render_views - shared_examples 'authenticate user' do - it 'redirects to sign_in page' do - expect(subject).to redirect_to new_user_session_path - end - end - context 'when user is not signed in' do subject { post :create } - it_behaves_like 'authenticate user' + it { is_expected.to redirect_to new_user_session_path } end context 'when user is signed in' do diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb index 93c5de0899..dca4c925fd 100644 --- a/spec/controllers/settings/migrations_controller_spec.rb +++ b/spec/controllers/settings/migrations_controller_spec.rb @@ -5,17 +5,11 @@ require 'rails_helper' RSpec.describe Settings::MigrationsController do render_views - shared_examples 'authenticate user' do - it 'redirects to sign_in page' do - expect(subject).to redirect_to new_user_session_path - end - end - describe 'GET #show' do context 'when user is not sign in' do subject { get :show } - it_behaves_like 'authenticate user' + it { is_expected.to redirect_to new_user_session_path } end context 'when user is sign in' do @@ -49,7 +43,7 @@ RSpec.describe Settings::MigrationsController do context 'when user is not sign in' do subject { post :create } - it_behaves_like 'authenticate user' + it { is_expected.to redirect_to new_user_session_path } end context 'when user is signed in' do diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb index 3f77d7665a..5a544c3d38 100644 --- a/spec/lib/permalink_redirector_spec.rb +++ b/spec/lib/permalink_redirector_spec.rb @@ -29,5 +29,20 @@ RSpec.describe PermalinkRedirector do redirector = described_class.new('@alice/123') expect(redirector.redirect_path).to eq 'https://example.com/status-123' end + + it 'returns path for legacy status links with a query param' do + redirector = described_class.new('statuses/123?foo=bar') + expect(redirector.redirect_path).to eq 'https://example.com/status-123' + end + + it 'returns path for pretty status links with a query param' do + redirector = described_class.new('@alice/123?foo=bar') + expect(redirector.redirect_path).to eq 'https://example.com/status-123' + end + + it 'returns path for deck URLs with query params' do + redirector = described_class.new('/deck/directory?local=true') + expect(redirector.redirect_path).to eq '/directory?local=true' + end end end diff --git a/spec/lib/scope_transformer_spec.rb b/spec/lib/scope_transformer_spec.rb index 09a31e04c5..f4003352e4 100644 --- a/spec/lib/scope_transformer_spec.rb +++ b/spec/lib/scope_transformer_spec.rb @@ -7,16 +7,13 @@ RSpec.describe ScopeTransformer do subject { described_class.new.apply(ScopeParser.new.parse(input)) } shared_examples 'a scope' do |namespace, term, access| - it 'parses the term' do - expect(subject.term).to eq term - end - - it 'parses the namespace' do - expect(subject.namespace).to eq namespace - end - - it 'parses the access' do - expect(subject.access).to eq access + it 'parses the attributes' do + expect(subject) + .to have_attributes( + term: term, + namespace: namespace, + access: access + ) end end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 056000b042..4c6107d9f7 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -3,6 +3,17 @@ require 'rails_helper' RSpec.describe NotificationMailer do + shared_examples 'delivery to non functional user' do + context 'when user is not functional' do + before { receiver.update(confirmed_at: nil) } + + it 'does not deliver mail' do + emails = capture_emails { mail.deliver_now } + expect(emails).to be_empty + end + end + end + let(:receiver) { Fabricate(:user, account_attributes: { username: 'alice' }) } let(:sender) { Fabricate(:account, username: 'bob') } let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') } @@ -24,6 +35,8 @@ RSpec.describe NotificationMailer do .and have_thread_headers .and have_standard_headers('mention').for(receiver) end + + include_examples 'delivery to non functional user' end describe 'follow' do @@ -40,6 +53,8 @@ RSpec.describe NotificationMailer do .and(have_body_text('bob is now following you')) .and have_standard_headers('follow').for(receiver) end + + include_examples 'delivery to non functional user' end describe 'favourite' do @@ -58,6 +73,8 @@ RSpec.describe NotificationMailer do .and have_thread_headers .and have_standard_headers('favourite').for(receiver) end + + include_examples 'delivery to non functional user' end describe 'reblog' do @@ -76,6 +93,8 @@ RSpec.describe NotificationMailer do .and have_thread_headers .and have_standard_headers('reblog').for(receiver) end + + include_examples 'delivery to non functional user' end describe 'follow_request' do @@ -92,6 +111,8 @@ RSpec.describe NotificationMailer do .and(have_body_text('bob has requested to follow you')) .and have_standard_headers('follow_request').for(receiver) end + + include_examples 'delivery to non functional user' end private diff --git a/spec/requests/api/v1/accounts/follower_accounts_spec.rb b/spec/requests/api/v1/accounts/follower_accounts_spec.rb index 7db9884a57..61987fac1c 100644 --- a/spec/requests/api/v1/accounts/follower_accounts_spec.rb +++ b/spec/requests/api/v1/accounts/follower_accounts_spec.rb @@ -23,8 +23,11 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq 2 - expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s), + hash_including(id: bob.id.to_s) + ) end it 'does not return blocked users', :aggregate_failures do @@ -34,8 +37,10 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq 1 - expect(response.parsed_body[0][:id]).to eq alice.id.to_s + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s) + ) end context 'when requesting user is blocked' do @@ -56,8 +61,11 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do account.mute!(bob) get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers - expect(response.parsed_body.size).to eq 2 - expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s), + hash_including(id: bob.id.to_s) + ) end end end diff --git a/spec/requests/api/v1/accounts/following_accounts_spec.rb b/spec/requests/api/v1/accounts/following_accounts_spec.rb index ffb7332c4e..aae811467d 100644 --- a/spec/requests/api/v1/accounts/following_accounts_spec.rb +++ b/spec/requests/api/v1/accounts/following_accounts_spec.rb @@ -23,8 +23,11 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq 2 - expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s), + hash_including(id: bob.id.to_s) + ) end it 'does not return blocked users', :aggregate_failures do @@ -34,8 +37,10 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq 1 - expect(response.parsed_body[0][:id]).to eq alice.id.to_s + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s) + ) end context 'when requesting user is blocked' do @@ -56,8 +61,11 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do account.mute!(bob) get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers - expect(response.parsed_body.size).to eq 2 - expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s), + hash_including(id: bob.id.to_s) + ) end end end diff --git a/spec/requests/api/v1/directories_spec.rb b/spec/requests/api/v1/directories_spec.rb index 282be9a582..07e65f49b7 100644 --- a/spec/requests/api/v1/directories_spec.rb +++ b/spec/requests/api/v1/directories_spec.rb @@ -84,8 +84,11 @@ RSpec.describe 'Directories API' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq(2) - expect(response.parsed_body.pluck(:id)).to contain_exactly(eligible_remote_account.id.to_s, local_discoverable_account.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: eligible_remote_account.id.to_s), + hash_including(id: local_discoverable_account.id.to_s) + ) end end @@ -105,9 +108,11 @@ RSpec.describe 'Directories API' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq(1) - expect(response.parsed_body.first[:id]).to include(local_account.id.to_s) - expect(response.body).to_not include(remote_account.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: local_account.id.to_s) + ) + .and not_include(remote_account.id.to_s) end end @@ -121,9 +126,11 @@ RSpec.describe 'Directories API' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq(2) - expect(response.parsed_body.first[:id]).to include(new_stat.account_id.to_s) - expect(response.parsed_body.second[:id]).to include(old_stat.account_id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: new_stat.account_id.to_s), + hash_including(id: old_stat.account_id.to_s) + ) end end @@ -138,9 +145,11 @@ RSpec.describe 'Directories API' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size).to eq(2) - expect(response.parsed_body.first[:id]).to include(account_new.id.to_s) - expect(response.parsed_body.second[:id]).to include(account_old.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: account_new.id.to_s), + hash_including(id: account_old.id.to_s) + ) end end end diff --git a/spec/requests/api/v1/peers/search_spec.rb b/spec/requests/api/v1/peers/search_spec.rb index d00a2437f2..afcc141902 100644 --- a/spec/requests/api/v1/peers/search_spec.rb +++ b/spec/requests/api/v1/peers/search_spec.rb @@ -55,10 +55,10 @@ RSpec.describe 'API Peers Search' do .to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size) - .to eq(1) - expect(response.parsed_body.first) - .to eq(account.domain) + expect(response.parsed_body) + .to contain_exactly( + eq(account.domain) + ) end end end diff --git a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb index 6471697154..441664d099 100644 --- a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb +++ b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb @@ -36,8 +36,6 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size) - .to eq(2) expect(response.parsed_body) .to contain_exactly( include(id: alice.id.to_s), @@ -50,9 +48,10 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do subject - expect(response.parsed_body.size) - .to eq 1 - expect(response.parsed_body.first[:id]).to eq(alice.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s) + ) end end end diff --git a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb index 40457f6e89..824b5aa275 100644 --- a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb +++ b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb @@ -35,8 +35,6 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body.size) - .to eq(2) expect(response.parsed_body) .to contain_exactly( include(id: alice.id.to_s), @@ -49,9 +47,10 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do subject - expect(response.parsed_body.size) - .to eq 1 - expect(response.parsed_body.first[:id]).to eq(alice.id.to_s) + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: alice.id.to_s) + ) end end end diff --git a/spec/requests/api/v2_alpha/notifications_spec.rb b/spec/requests/api/v2_alpha/notifications_spec.rb deleted file mode 100644 index 6d7df45b65..0000000000 --- a/spec/requests/api/v2_alpha/notifications_spec.rb +++ /dev/null @@ -1,345 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this before 4.3.0-rc1 - -require 'rails_helper' - -RSpec.describe 'Notifications' do - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:notifications write:notifications' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2_alpha/notifications/unread_count', :inline_jobs do - subject do - get '/api/v2_alpha/notifications/unread_count', headers: headers, params: params - end - - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(Fabricate(:account), first_status) - PostStatusService.new.call(Fabricate(:account), text: 'Hello @alice') - FavouriteService.new.call(Fabricate(:account), first_status) - FavouriteService.new.call(Fabricate(:account), first_status) - FollowService.new.call(Fabricate(:account), user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq 4 - end - end - - context 'with grouped_types parameter' do - let(:params) { { grouped_types: %w(reblog) } } - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq 5 - end - end - - context 'with a read marker' do - before do - id = user.account.notifications.browserable.order(id: :desc).offset(2).first.id - user.markers.create!(timeline: 'notifications', last_read_id: id) - end - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq 2 - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq 3 - end - end - - context 'with a user-provided limit' do - let(:params) { { limit: 2 } } - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq 2 - end - end - - context 'when there are more notifications than the limit' do - before do - stub_const('Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2) - end - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:count]).to eq Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT - end - end - end - - describe 'GET /api/v2_alpha/notifications', :inline_jobs do - subject do - get '/api/v2_alpha/notifications', headers: headers, params: params - end - - let(:bob) { Fabricate(:user) } - let(:tom) { Fabricate(:user) } - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(bob.account, first_status) - PostStatusService.new.call(bob.account, text: 'Hello @alice') - FavouriteService.new.call(bob.account, first_status) - FavouriteService.new.call(tom.account, first_status) - FollowService.new.call(bob.account, user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'when there are no notifications' do - before do - user.account.notifications.destroy_all - end - - it 'returns 0 notifications' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:notification_groups]).to eq [] - end - end - - context 'with no options' do - it 'returns expected notification types', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow') - end - end - - context 'with grouped_types param' do - let(:params) { { grouped_types: %w(reblog) } } - - it 'returns everything, but does not group favourites' do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:notification_groups]).to contain_exactly( - a_hash_including( - type: 'reblog', - sample_account_ids: [bob.account_id.to_s] - ), - a_hash_including( - type: 'mention', - sample_account_ids: [bob.account_id.to_s] - ), - a_hash_including( - type: 'favourite', - sample_account_ids: [bob.account_id.to_s] - ), - a_hash_including( - type: 'favourite', - sample_account_ids: [tom.account_id.to_s] - ), - a_hash_including( - type: 'follow', - sample_account_ids: [bob.account_id.to_s] - ) - ) - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns everything but excluded type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body.size).to_not eq 0 - expect(body_json_types.uniq).to_not include 'mention' - end - end - - context 'with types param' do - let(:params) { { types: %w(mention) } } - - it 'returns only requested type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_types.uniq).to eq ['mention'] - expect(response.parsed_body.dig(:notification_groups, 0, :page_min_id)).to_not be_nil - end - end - - context 'with limit param' do - let(:params) { { limit: 3 } } - let(:notifications) { user.account.notifications.reorder(id: :desc) } - - it 'returns the requested number of notifications paginated', :aggregate_failures do - subject - - expect(response.parsed_body[:notification_groups].size) - .to eq(params[:limit]) - - expect(response) - .to include_pagination_headers( - prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id), - # TODO: one downside of the current approach is that we return the first ID matching the group, - # not the last that has been skipped, so pagination is very likely to give overlap - next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[3].id) - ) - end - end - - context 'with since_id param' do - let(:params) { { since_id: notifications[2].id } } - let(:notifications) { user.account.notifications.reorder(id: :desc) } - - it 'returns the requested number of notifications paginated', :aggregate_failures do - subject - - expect(response.parsed_body[:notification_groups].size) - .to eq(2) - - expect(response) - .to include_pagination_headers( - prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id), - # TODO: one downside of the current approach is that we return the first ID matching the group, - # not the last that has been skipped, so pagination is very likely to give overlap - next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[1].id) - ) - end - end - - context 'when requesting stripped-down accounts' do - let(:params) { { expand_accounts: 'partial_avatars' } } - - let(:recent_account) { Fabricate(:account) } - - before do - FavouriteService.new.call(recent_account, user.account.statuses.first) - end - - it 'returns an account in "partial_accounts", with the expected keys', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(response.parsed_body[:partial_accounts].size).to be > 0 - expect(response.parsed_body[:partial_accounts][0].keys.map(&:to_sym)).to contain_exactly(:acct, :avatar, :avatar_static, :bot, :id, :locked, :url) - expect(response.parsed_body[:partial_accounts].pluck(:id)).to_not include(recent_account.id.to_s) - expect(response.parsed_body[:accounts].pluck(:id)).to include(recent_account.id.to_s) - end - end - - context 'when passing an invalid value for "expand_accounts"' do - let(:params) { { expand_accounts: 'unknown_foobar' } } - - it 'returns http bad request' do - subject - - expect(response).to have_http_status(400) - end - end - - def body_json_types - response.parsed_body[:notification_groups].pluck(:type) - end - end - - describe 'GET /api/v2_alpha/notifications/:id' do - subject do - get "/api/v2_alpha/notifications/#{notification.group_key}", headers: headers - end - - let(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') } - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification, group_key: 'foobar') } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v2_alpha/notifications/:id/dismiss' do - subject do - post "/api/v2_alpha/notifications/#{notification.group_key}/dismiss", headers: headers - end - - let!(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'destroys the notification' do - subject - - expect(response).to have_http_status(200) - expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v2_alpha/notifications/clear' do - subject do - post '/api/v2_alpha/notifications/clear', headers: headers - end - - before do - Fabricate(:notification, account: user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'clears notifications for the account' do - subject - - expect(user.account.reload.notifications).to be_empty - expect(response).to have_http_status(200) - end - end -end