mirror of
https://github.com/funamitech/mastodon
synced 2024-12-03 17:28:35 +09:00
Merge pull request #2858 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 28966fa0a6
This commit is contained in:
commit
f610fdd6e7
@ -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)
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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'>
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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)",
|
||||
|
@ -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)",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}>",
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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á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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user