1
0

Merge pull request #2858 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 28966fa0a6
This commit is contained in:
Claire 2024-09-25 21:15:51 +02:00 committed by GitHub
commit f610fdd6e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 409 additions and 651 deletions

View File

@ -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)

View File

@ -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?

View File

@ -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

View File

@ -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;

View File

@ -315,36 +315,48 @@ class StatusActionBar extends ImmutablePureComponent {
}
const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
</div>
);
return (
<div className='status__action-bar'>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={replyIcon}
iconComponent={replyIconComponent}
onClick={this.handleReplyClick}
counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount
/>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
<div className='status__action-bar__button-wrapper'>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={replyIcon}
iconComponent={replyIconComponent}
onClick={this.handleReplyClick}
counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount
/>
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
{filterButton}
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
size={18}
iconComponent={MoreHorizIcon}
direction='right'
ariaLabel={intl.formatMessage(messages.more)}
/>
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
size={18}
iconComponent={MoreHorizIcon}
direction='right'
ariaLabel={intl.formatMessage(messages.more)}
/>
</div>
<div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>

View File

@ -196,7 +196,7 @@ class SwitchingColumnsArea extends PureComponent {
{redirect}
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={{...this.props.location, pathname: pathName.slice(5)}} /> : null}
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}

View File

@ -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;
@ -1516,6 +1520,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);
}

View File

@ -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

View File

@ -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;

View File

@ -375,20 +375,29 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
);
}

View File

@ -186,7 +186,7 @@ class SwitchingColumnsArea extends PureComponent {
{redirect}
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={{...this.props.location, pathname: pathName.slice(5)}} /> : null}
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}

View File

@ -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)",

View File

@ -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 lavertissement de contenu",
"compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu",
"compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)",

View File

@ -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 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",

View File

@ -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);
}

View File

@ -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

View File

@ -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}>",

View File

@ -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?

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>"
prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>
review_permissions: Revisar permisos
title: Autorización requerida
show:

View File

@ -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. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>"
prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>
review_permissions: Revisar permisos
title: Se requiere autorización
show:

View File

@ -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. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>"
prompt_html: A %{client_name} le gustaría obtener permiso para acceder a tu cuenta. <strong>Solo aprueba esta solicitud si reconoces y confías en esta fuente.</strong>
review_permissions: Revisar permisos
title: Se requiere autorización
show:

View File

@ -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. <strong>Csak akkor engedélyezd ezt a kérést, ha felismered és megbízol ebben a forrásban.</strong>
review_permissions: Jogosultságok áttekintése
title: Engedélyezés szükséges
show:

View File

@ -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

View File

@ -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

View File

@ -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 <strong>%{count}</strong> 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: "<strong>Mẹo:</strong> 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 bản ghi mới
overwrite_preambles:
blocking_html: Bạn sắp <strong>thay thế danh sách chặn</strong> với <strong>%{total_items} tài khoản</strong> từ <strong>%{filename}</strong>.
bookmarks_html: Bạn sắp <strong>thay thế lượt lưu</strong> với <strong>%{total_items} tút</strong> từ <strong>%{filename}</strong>.
@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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