mirror of
https://github.com/funamitech/mastodon
synced 2024-12-12 13:48: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)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.977.0)
|
aws-partitions (1.978.0)
|
||||||
aws-sdk-core (3.208.0)
|
aws-sdk-core (3.209.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
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-sdk-core (~> 3, >= 3.207.0)
|
||||||
aws-sigv4 (~> 1.5)
|
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-core (~> 3, >= 3.207.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
|
@ -31,7 +31,7 @@ module WebAppControllerConcern
|
|||||||
def redirect_unauthenticated_to_permalinks!
|
def redirect_unauthenticated_to_permalinks!
|
||||||
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
|
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?
|
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?
|
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));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
'notificationGroups/fetch',
|
'notificationGroups/fetch',
|
||||||
async (_params, { getState }) =>
|
async (_params, { getState }) =>
|
||||||
apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
|
apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
|
exclude_types: getExcludedTypes(getState()),
|
||||||
|
}),
|
||||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
@ -93,6 +98,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||||||
'notificationGroups/fetchGap',
|
'notificationGroups/fetchGap',
|
||||||
async (params: { gap: NotificationGap }, { getState }) =>
|
async (params: { gap: NotificationGap }, { getState }) =>
|
||||||
apiFetchNotificationGroups({
|
apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
max_id: params.gap.maxId,
|
max_id: params.gap.maxId,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
}),
|
}),
|
||||||
@ -109,6 +115,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||||||
'notificationGroups/pollRecentNotifications',
|
'notificationGroups/pollRecentNotifications',
|
||||||
async (_params, { getState }) => {
|
async (_params, { getState }) => {
|
||||||
return apiFetchNotificationGroups({
|
return apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
max_id: undefined,
|
max_id: undefined,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
// 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?: {
|
export const apiFetchNotificationGroups = async (params?: {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
grouped_types?: string[];
|
||||||
exclude_types?: string[];
|
exclude_types?: string[];
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
|
@ -315,11 +315,14 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filterButton = this.props.onFilter && (
|
const filterButton = this.props.onFilter && (
|
||||||
|
<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} />
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
|
<div className='status__action-bar__button-wrapper'>
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button'
|
className='status__action-bar-button'
|
||||||
title={replyTitle}
|
title={replyTitle}
|
||||||
@ -329,12 +332,20 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
counter={showReplyCount ? status.get('replies_count') : undefined}
|
counter={showReplyCount ? status.get('replies_count') : undefined}
|
||||||
obfuscateCount
|
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} />
|
<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} />
|
<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} />
|
<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}
|
{filterButton}
|
||||||
|
|
||||||
|
<div className='status__action-bar__button-wrapper'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
status={status}
|
status={status}
|
||||||
@ -345,6 +356,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
direction='right'
|
direction='right'
|
||||||
ariaLabel={intl.formatMessage(messages.more)}
|
ariaLabel={intl.formatMessage(messages.more)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='status__action-bar-spacer' />
|
<div className='status__action-bar-spacer' />
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
||||||
|
@ -196,7 +196,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
{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 */}
|
{/* 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 === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
||||||
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
|
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
&:disabled,
|
&:disabled,
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background-color: $ui-primary-color;
|
background-color: $ui-primary-color;
|
||||||
cursor: default;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.copyable {
|
&.copyable {
|
||||||
@ -299,6 +299,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--with-counter {
|
||||||
|
padding-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&__counter {
|
&__counter {
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
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 {
|
&--first-in-thread {
|
||||||
border-top: 1px solid var(--background-border-color);
|
border-top: 1px solid var(--background-border-color);
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,15 @@ function dispatchAssociatedRecords(
|
|||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
'notificationGroups/fetch',
|
'notificationGroups/fetch',
|
||||||
async (_params, { getState }) =>
|
async (_params, { getState }) =>
|
||||||
apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
|
apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
|
exclude_types: getExcludedTypes(getState()),
|
||||||
|
}),
|
||||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
@ -93,6 +98,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||||||
'notificationGroups/fetchGap',
|
'notificationGroups/fetchGap',
|
||||||
async (params: { gap: NotificationGap }, { getState }) =>
|
async (params: { gap: NotificationGap }, { getState }) =>
|
||||||
apiFetchNotificationGroups({
|
apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
max_id: params.gap.maxId,
|
max_id: params.gap.maxId,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
}),
|
}),
|
||||||
@ -109,6 +115,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||||||
'notificationGroups/pollRecentNotifications',
|
'notificationGroups/pollRecentNotifications',
|
||||||
async (_params, { getState }) => {
|
async (_params, { getState }) => {
|
||||||
return apiFetchNotificationGroups({
|
return apiFetchNotificationGroups({
|
||||||
|
grouped_types: supportedGroupedNotificationTypes,
|
||||||
max_id: undefined,
|
max_id: undefined,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
// 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?: {
|
export const apiFetchNotificationGroups = async (params?: {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
grouped_types?: string[];
|
||||||
exclude_types?: string[];
|
exclude_types?: string[];
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
|
@ -375,11 +375,19 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
|
<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')} />
|
<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} />
|
<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} />
|
<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} />
|
<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
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
status={status}
|
status={status}
|
||||||
@ -390,6 +398,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
title={intl.formatMessage(messages.more)}
|
title={intl.formatMessage(messages.more)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
{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 */}
|
{/* 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 === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
||||||
{!singleColumn && pathName === '/home' ? <Redirect from='/home' 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": "Publier",
|
||||||
"compose_form.publish_form": "Publier",
|
"compose_form.publish_form": "Publier",
|
||||||
"compose_form.reply": "Répondre",
|
"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.marked": "Enlever l'avertissement de contenu",
|
||||||
"compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu",
|
"compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu",
|
||||||
"compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)",
|
"compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)",
|
||||||
|
@ -164,7 +164,7 @@
|
|||||||
"compose_form.publish": "Publier",
|
"compose_form.publish": "Publier",
|
||||||
"compose_form.publish_form": "Nouvelle publication",
|
"compose_form.publish_form": "Nouvelle publication",
|
||||||
"compose_form.reply": "Répondre",
|
"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.marked": "Enlever l’avertissement de contenu",
|
||||||
"compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu",
|
"compose_form.spoiler.unmarked": "Ajouter un avertissement de contenu",
|
||||||
"compose_form.spoiler_placeholder": "Avertissement de contenu (optionnel)",
|
"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.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký",
|
||||||
"admin.dashboard.retention.average": "Trung bình",
|
"admin.dashboard.retention.average": "Trung bình",
|
||||||
"admin.dashboard.retention.cohort": "Tháng đăng ký",
|
"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_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_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",
|
"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": "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.lock_disclaimer.lock": "khóa",
|
||||||
"compose_form.placeholder": "Bạn đang nghĩ gì?",
|
"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.multiple": "Chọn nhiều",
|
||||||
"compose_form.poll.option_placeholder": "Lựa chọn {number}",
|
"compose_form.poll.option_placeholder": "Lựa chọn {number}",
|
||||||
"compose_form.poll.single": "Chọn một",
|
"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.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.confirm": "Sửa",
|
||||||
"confirmations.edit.message": "Nội dung tút cũ sẽ bị ghi đè, bạn có tiếp tục?",
|
"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.confirm": "Đăng xuất",
|
||||||
"confirmations.logout.message": "Bạn có chắc muốn thoát?",
|
"confirmations.logout.message": "Bạn có chắc muốn thoát?",
|
||||||
"confirmations.logout.title": "Đăng xuất",
|
"confirmations.logout.title": "Đăng xuất",
|
||||||
@ -190,11 +190,11 @@
|
|||||||
"confirmations.redraft.title": "Xóa & viết lại",
|
"confirmations.redraft.title": "Xóa & viết lại",
|
||||||
"confirmations.reply.confirm": "Trả 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.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.confirm": "Bỏ theo dõi",
|
||||||
"confirmations.unfollow.message": "Bạn có chắc muốn bỏ theo dõi {name}?",
|
"confirmations.unfollow.message": "Bạn có chắc muốn bỏ theo dõi {name}?",
|
||||||
"confirmations.unfollow.title": "Bỏ theo dõi",
|
"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",
|
"content_warning.show": "Nhấn để xem",
|
||||||
"conversation.delete": "Xóa tin nhắn này",
|
"conversation.delete": "Xóa tin nhắn này",
|
||||||
"conversation.mark_as_read": "Đánh dấu là đã đọc",
|
"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.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.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.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.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.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ả",
|
"follow_suggestions.view_all": "Xem tất cả",
|
||||||
@ -480,7 +480,7 @@
|
|||||||
"navigation_bar.domain_blocks": "Máy chủ đã ẩn",
|
"navigation_bar.domain_blocks": "Máy chủ đã ẩn",
|
||||||
"navigation_bar.explore": "Xu hướng",
|
"navigation_bar.explore": "Xu hướng",
|
||||||
"navigation_bar.favourites": "Tút thích",
|
"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.follow_requests": "Yêu cầu theo dõi",
|
||||||
"navigation_bar.followed_tags": "Hashtag theo dõi",
|
"navigation_bar.followed_tags": "Hashtag theo dõi",
|
||||||
"navigation_bar.follows_and_followers": "Quan hệ",
|
"navigation_bar.follows_and_followers": "Quan hệ",
|
||||||
@ -555,7 +555,7 @@
|
|||||||
"notification_requests.view": "Hiện thông báo",
|
"notification_requests.view": "Hiện thông báo",
|
||||||
"notifications.clear": "Xóa hết 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_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.report": "Báo cáo mới:",
|
||||||
"notifications.column_settings.admin.sign_up": "Người mới tham gia:",
|
"notifications.column_settings.admin.sign_up": "Người mới tham gia:",
|
||||||
"notifications.column_settings.alert": "Báo trên máy tính",
|
"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_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_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_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_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ắc riêng tư không được yêu cầu",
|
"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.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.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.",
|
"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": "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.other_description": "Vấn đề không nằm trong những mục trên",
|
||||||
"report.reasons.spam": "Đây là spam",
|
"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": "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.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",
|
"report.rules.subtitle": "Chọn tất cả những gì phù hợp",
|
||||||
@ -787,9 +787,9 @@
|
|||||||
"status.edit": "Sửa",
|
"status.edit": "Sửa",
|
||||||
"status.edited": "Sửa lần cuối {date}",
|
"status.edited": "Sửa lần cuối {date}",
|
||||||
"status.edited_x_times": "Đã sửa {count, plural, other {{count} lần}}",
|
"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.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.filter": "Lọc tút này",
|
||||||
"status.history.created": "{name} đăng {date}",
|
"status.history.created": "{name} đăng {date}",
|
||||||
"status.history.edited": "{name} đã sửa {date}",
|
"status.history.edited": "{name} đã sửa {date}",
|
||||||
@ -808,7 +808,7 @@
|
|||||||
"status.reblog": "Đăng lại",
|
"status.reblog": "Đăng lại",
|
||||||
"status.reblog_private": "Đăng lại (Riêng tư)",
|
"status.reblog_private": "Đăng lại (Riêng tư)",
|
||||||
"status.reblogged_by": "{name} đăng lại",
|
"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.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.redraft": "Xóa và viết lại",
|
||||||
"status.remove_bookmark": "Bỏ lưu",
|
"status.remove_bookmark": "Bỏ lưu",
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
&:disabled,
|
&:disabled,
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background-color: $ui-primary-color;
|
background-color: $ui-primary-color;
|
||||||
cursor: default;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.copyable {
|
&.copyable {
|
||||||
@ -299,6 +299,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--with-counter {
|
||||||
|
padding-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&__counter {
|
&__counter {
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
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 {
|
&--first-in-thread {
|
||||||
border-top: 1px solid var(--background-border-color);
|
border-top: 1px solid var(--background-border-color);
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,6 @@ class PermalinkRedirector
|
|||||||
end
|
end
|
||||||
|
|
||||||
def path_segments
|
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
|
||||||
end
|
end
|
||||||
|
@ -13,12 +13,14 @@ class NotificationMailer < ApplicationMailer
|
|||||||
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
|
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
|
||||||
after_action :set_list_headers!
|
after_action :set_list_headers!
|
||||||
|
|
||||||
|
before_deliver :verify_functional_user
|
||||||
|
|
||||||
default to: -> { email_address_with_name(@user.email, @me.username) }
|
default to: -> { email_address_with_name(@user.email, @me.username) }
|
||||||
|
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
|
||||||
def mention
|
def mention
|
||||||
return unless @user.functional? && @status.present?
|
return if @status.blank?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail subject: default_i18n_subject(name: @status.account.acct)
|
mail subject: default_i18n_subject(name: @status.account.acct)
|
||||||
@ -26,15 +28,13 @@ class NotificationMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
return unless @user.functional?
|
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail subject: default_i18n_subject(name: @account.acct)
|
mail subject: default_i18n_subject(name: @account.acct)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourite
|
def favourite
|
||||||
return unless @user.functional? && @status.present?
|
return if @status.blank?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail subject: default_i18n_subject(name: @account.acct)
|
mail subject: default_i18n_subject(name: @account.acct)
|
||||||
@ -42,7 +42,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reblog
|
def reblog
|
||||||
return unless @user.functional? && @status.present?
|
return if @status.blank?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail subject: default_i18n_subject(name: @account.acct)
|
mail subject: default_i18n_subject(name: @account.acct)
|
||||||
@ -50,8 +50,6 @@ class NotificationMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def follow_request
|
def follow_request
|
||||||
return unless @user.functional?
|
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail subject: default_i18n_subject(name: @account.acct)
|
mail subject: default_i18n_subject(name: @account.acct)
|
||||||
end
|
end
|
||||||
@ -75,6 +73,10 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@account = @notification.from_account
|
@account = @notification.from_account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def verify_functional_user
|
||||||
|
throw(:abort) unless @user.functional?
|
||||||
|
end
|
||||||
|
|
||||||
def set_list_headers!
|
def set_list_headers!
|
||||||
headers(
|
headers(
|
||||||
'List-ID' => "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>",
|
'List-ID' => "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>",
|
||||||
|
@ -20,6 +20,7 @@ class Notification < ApplicationRecord
|
|||||||
self.inheritance_column = nil
|
self.inheritance_column = nil
|
||||||
|
|
||||||
include Paginable
|
include Paginable
|
||||||
|
include Redisable
|
||||||
|
|
||||||
LEGACY_TYPE_CLASS_MAP = {
|
LEGACY_TYPE_CLASS_MAP = {
|
||||||
'Mention' => :mention,
|
'Mention' => :mention,
|
||||||
@ -30,7 +31,9 @@ class Notification < ApplicationRecord
|
|||||||
'Poll' => :poll,
|
'Poll' => :poll,
|
||||||
}.freeze
|
}.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
|
# Please update app/javascript/api_types/notification.ts if you change this
|
||||||
PROPERTIES = {
|
PROPERTIES = {
|
||||||
@ -123,6 +126,30 @@ class Notification < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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
|
class << self
|
||||||
def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false)
|
def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false)
|
||||||
requested_types = if types.empty?
|
requested_types = if types.empty?
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
class NotifyService < BaseService
|
class NotifyService < BaseService
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
MAXIMUM_GROUP_SPAN_HOURS = 12
|
|
||||||
|
|
||||||
# TODO: the severed_relationships type probably warrants email notifications
|
# TODO: the severed_relationships type probably warrants email notifications
|
||||||
NON_EMAIL_TYPES = %i(
|
NON_EMAIL_TYPES = %i(
|
||||||
admin.report
|
admin.report
|
||||||
@ -216,7 +214,7 @@ class NotifyService < BaseService
|
|||||||
return if drop?
|
return if drop?
|
||||||
|
|
||||||
@notification.filtered = filter?
|
@notification.filtered = filter?
|
||||||
@notification.group_key = notification_group_key
|
@notification.set_group_key!
|
||||||
@notification.save!
|
@notification.save!
|
||||||
|
|
||||||
# It's possible the underlying activity has been deleted
|
# It's possible the underlying activity has been deleted
|
||||||
@ -236,23 +234,6 @@ class NotifyService < BaseService
|
|||||||
|
|
||||||
private
|
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?
|
def drop?
|
||||||
DropCondition.new(@notification).drop?
|
DropCondition.new(@notification).drop?
|
||||||
end
|
end
|
||||||
|
@ -15,6 +15,12 @@ eo:
|
|||||||
user/invite_request:
|
user/invite_request:
|
||||||
text: Kialo
|
text: Kialo
|
||||||
errors:
|
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:
|
models:
|
||||||
account:
|
account:
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -48,10 +48,13 @@ eo:
|
|||||||
subject: 'Mastodon: Instrukcioj por ŝanĝi pasvorton'
|
subject: 'Mastodon: Instrukcioj por ŝanĝi pasvorton'
|
||||||
title: Pasvorto restarigita
|
title: Pasvorto restarigita
|
||||||
two_factor_disabled:
|
two_factor_disabled:
|
||||||
|
explanation: Ensalutu nun eblas uzante nur retadreson kaj pasvorton.
|
||||||
subject: 'Mastodon: dufaktora aŭtentigo malebligita'
|
subject: 'Mastodon: dufaktora aŭtentigo malebligita'
|
||||||
|
subtitle: Dupaŝa aŭtentigo por via konto estas malŝaltita.
|
||||||
title: 2FA estas malŝaltita
|
title: 2FA estas malŝaltita
|
||||||
two_factor_enabled:
|
two_factor_enabled:
|
||||||
subject: 'Mastodon: Dufaktora aŭtentigo ebligita'
|
subject: 'Mastodon: Dufaktora aŭtentigo ebligita'
|
||||||
|
subtitle: Dupaŝa aŭtentigo por via konto estas ŝaltita.
|
||||||
title: 2FA aktivigita
|
title: 2FA aktivigita
|
||||||
two_factor_recovery_codes_changed:
|
two_factor_recovery_codes_changed:
|
||||||
explanation: La antaŭaj reakiraj kodoj estis nuligitaj kaj novaj estis generitaj.
|
explanation: La antaŭaj reakiraj kodoj estis nuligitaj kaj novaj estis generitaj.
|
||||||
|
@ -60,7 +60,7 @@ es-AR:
|
|||||||
error:
|
error:
|
||||||
title: Ocurrió un error
|
title: Ocurrió un error
|
||||||
new:
|
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
|
review_permissions: Revisar permisos
|
||||||
title: Autorización requerida
|
title: Autorización requerida
|
||||||
show:
|
show:
|
||||||
|
@ -60,7 +60,7 @@ es-MX:
|
|||||||
error:
|
error:
|
||||||
title: Ha ocurrido un error
|
title: Ha ocurrido un error
|
||||||
new:
|
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
|
review_permissions: Revisar permisos
|
||||||
title: Se requiere autorización
|
title: Se requiere autorización
|
||||||
show:
|
show:
|
||||||
|
@ -60,7 +60,7 @@ es:
|
|||||||
error:
|
error:
|
||||||
title: Ha ocurrido un error
|
title: Ha ocurrido un error
|
||||||
new:
|
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
|
review_permissions: Revisar permisos
|
||||||
title: Se requiere autorización
|
title: Se requiere autorización
|
||||||
show:
|
show:
|
||||||
|
@ -60,6 +60,7 @@ hu:
|
|||||||
error:
|
error:
|
||||||
title: Hiba történt
|
title: Hiba történt
|
||||||
new:
|
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
|
review_permissions: Jogosultságok áttekintése
|
||||||
title: Engedélyezés szükséges
|
title: Engedélyezés szükséges
|
||||||
show:
|
show:
|
||||||
|
@ -71,7 +71,7 @@ vi:
|
|||||||
confirmations:
|
confirmations:
|
||||||
revoke: Bạn có chắc không?
|
revoke: Bạn có chắc không?
|
||||||
index:
|
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 đó.
|
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}
|
last_used_at: Dùng lần cuối %{date}
|
||||||
never_used: Chưa dùng
|
never_used: Chưa dùng
|
||||||
@ -151,7 +151,7 @@ vi:
|
|||||||
scopes:
|
scopes:
|
||||||
admin:read: đọc mọi dữ liệu trên máy chủ
|
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: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_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: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
|
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
|
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
|
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
|
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_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
|
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)
|
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
|
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
|
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
|
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
|
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ỏ.
|
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
|
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:
|
filters:
|
||||||
action: Chọn hành động sẽ thực hiện khi một tút khớp với bộ lọc
|
action: Chọn hành động sẽ thực hiện khi một tút khớp với bộ lọc
|
||||||
actions:
|
actions:
|
||||||
hide: Ẩn hoàn toàn nội dung đã lọc, như thể nó không tồn tại
|
hide: Ẩn hoàn toàn, 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
|
warn: Hiện cảnh báo và bộ lọc
|
||||||
form_admin_settings:
|
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
|
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.
|
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_theme: Giao diện
|
||||||
setting_trends: Hiển thị xu hướng trong ngày
|
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_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
|
setting_use_pending_items: Không tự động cập nhật bảng tin
|
||||||
severity: Mức độ nghiêm trọng
|
severity: Mức độ nghiêm trọng
|
||||||
sign_in_token_attempt: Mã an toàn
|
sign_in_token_attempt: Mã an toàn
|
||||||
@ -305,7 +305,7 @@ vi:
|
|||||||
label: Đã có phiên bản Mastodon mới
|
label: Đã có phiên bản Mastodon mới
|
||||||
none: Không bao giờ thông báo (không đề xuất)
|
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
|
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:
|
rule:
|
||||||
hint: Thông tin thêm
|
hint: Thông tin thêm
|
||||||
text: Nội quy
|
text: Nội quy
|
||||||
|
@ -44,7 +44,7 @@ vi:
|
|||||||
submit: Thay đổi email
|
submit: Thay đổi email
|
||||||
title: Thay đổi email cho %{username}
|
title: Thay đổi email cho %{username}
|
||||||
change_role:
|
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
|
edit_roles: Quản lý vai trò người dùng
|
||||||
label: Đổi vai trò
|
label: Đổi vai trò
|
||||||
no_role: Chưa có vai trò
|
no_role: Chưa có vai trò
|
||||||
@ -55,7 +55,7 @@ vi:
|
|||||||
custom: Tùy chỉnh
|
custom: Tùy chỉnh
|
||||||
delete: Xóa dữ liệu
|
delete: Xóa dữ liệu
|
||||||
deleted: Đã xóa
|
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ờ
|
destroyed_msg: Dữ liệu %{username} sẽ được lên lịch xóa ngay bây giờ
|
||||||
disable: Khóa
|
disable: Khóa
|
||||||
disable_sign_in_token_auth: Tắt xác minh bằng email
|
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: Lịch sử kiểm duyệt
|
||||||
previous_strikes_description_html:
|
previous_strikes_description_html:
|
||||||
other: Người này bị cảnh cáo <strong>%{count}</strong> lần.
|
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
|
protocol: Giao thức
|
||||||
public: Công khai
|
public: Công khai
|
||||||
push_subscription_expires: Đăng ký PuSH hết hạn
|
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_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.
|
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
|
title: Tài khoản
|
||||||
unblock_email: Mở khóa địa chỉ email
|
unblock_email: Bỏ chặn địa chỉ email
|
||||||
unblocked_email_msg: Mở khóa thành công địa chỉ email của %{username}
|
unblocked_email_msg: Đã bỏ chặn địa chỉ email của %{username}
|
||||||
unconfirmed_email: Email chưa được xác minh
|
unconfirmed_email: Email chưa được xác minh
|
||||||
undo_sensitized: Đánh dấu bình thường
|
undo_sensitized: Đánh dấu bình thường
|
||||||
undo_silenced: Bỏ hạn chế
|
undo_silenced: Bỏ hạn chế
|
||||||
@ -170,42 +170,42 @@ vi:
|
|||||||
action_logs:
|
action_logs:
|
||||||
action_types:
|
action_types:
|
||||||
approve_appeal: Chấp nhận kháng cáo
|
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
|
assigned_to_self_report: Tự xử lý báo cáo
|
||||||
change_email_user: Đổi email người dùng
|
change_email_user: Đổi email người dùng
|
||||||
change_role_user: Đổi vai trò
|
change_role_user: Đổi vai trò
|
||||||
confirm_user: Xác minh
|
confirm_user: Xác minh
|
||||||
create_account_warning: Cảnh cáo
|
create_account_warning: Cảnh cáo
|
||||||
create_announcement: Tạo thông báo mới
|
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_custom_emoji: Tạo emoji
|
||||||
create_domain_allow: Cho phép máy chủ
|
create_domain_allow: Cho phép máy chủ
|
||||||
create_domain_block: Chặn 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_email_domain_block: Tạo chặn tên miền email
|
||||||
create_ip_block: Tạo chặn IP mới
|
create_ip_block: Chặn IP
|
||||||
create_unavailable_domain: Máy chủ không khả dụng
|
create_unavailable_domain: Ngừng liên hợp
|
||||||
create_user_role: Tạo vai trò
|
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_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_custom_emoji: Xóa emoji
|
||||||
destroy_domain_allow: Bỏ thanh trừng máy chủ
|
destroy_domain_allow: Bỏ thanh trừng máy chủ
|
||||||
destroy_domain_block: Bỏ chặn 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_email_domain_block: Bỏ chặn tên miền email
|
||||||
destroy_instance: Thanh trừng máy chủ
|
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_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ò
|
destroy_user_role: Xóa vai trò
|
||||||
disable_2fa_user: Vô hiệu hóa 2FA
|
disable_2fa_user: Vô hiệu hóa 2FA
|
||||||
disable_custom_emoji: Vô hiệu hóa emoji
|
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_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
|
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_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
|
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_appeal: Từ chối kháng cáo
|
||||||
reject_user: Từ chối đăng ký
|
reject_user: Từ chối đăng ký
|
||||||
remove_avatar_user: Xóa ảnh đại diện
|
remove_avatar_user: Xóa ảnh đại diện
|
||||||
@ -213,11 +213,11 @@ vi:
|
|||||||
resend_user: Gửi lại email xác minh
|
resend_user: Gửi lại email xác minh
|
||||||
reset_password_user: Đặt lại mật khẩu
|
reset_password_user: Đặt lại mật khẩu
|
||||||
resolve_report: Xử lý báo cáo
|
resolve_report: Xử lý báo cáo
|
||||||
sensitive_account: Áp đặt nhạy cảm
|
sensitive_account: Gán nhạy cảm
|
||||||
silence_account: Áp đặt ẩn
|
silence_account: Gán ẩn
|
||||||
suspend_account: Áp đặt vô hiệu hóa
|
suspend_account: Gán vô hiệu hóa
|
||||||
unassigned_report: Báo cáo chưa xử lý
|
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
|
unsensitive_account: Bỏ nhạy cảm
|
||||||
unsilence_account: Bỏ ẩn
|
unsilence_account: Bỏ ẩn
|
||||||
unsuspend_account: Bỏ vô hiệu hóa
|
unsuspend_account: Bỏ vô hiệu hóa
|
||||||
@ -229,7 +229,7 @@ vi:
|
|||||||
update_status: Cập nhật tút
|
update_status: Cập nhật tút
|
||||||
update_user_role: Cập nhật vai trò
|
update_user_role: Cập nhật vai trò
|
||||||
actions:
|
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}"
|
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}"
|
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}"
|
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}"
|
confirm_user_html: "%{name} đã xác minh địa chỉ email của %{target}"
|
||||||
create_account_warning_html: "%{name} đã cảnh cáo %{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_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_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_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}"
|
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_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_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}"
|
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_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_custom_emoji_html: "%{name} đã xóa emoji %{target}"
|
||||||
destroy_domain_allow_html: "%{name} đã ngừng liên hợp với %{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}"
|
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_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_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}"
|
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_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}"
|
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"
|
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_appeal_html: "%{name} đã từ chối kháng cáo của %{target}"
|
||||||
reject_user_html: "%{name} đã từ chối đăng ký từ %{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}"
|
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}"
|
silence_account_html: "%{name} đã ẩn %{target}"
|
||||||
suspend_account_html: "%{name} đã vô hiệu hóa %{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í"
|
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"
|
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}"
|
unsilence_account_html: "%{name} đã bỏ ẩn %{target}"
|
||||||
unsuspend_account_html: "%{name} đã bỏ vô hiệu hóa %{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_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_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_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
|
deleted_account: tài khoản đã xóa
|
||||||
empty: Không tìm thấy bản ghi.
|
empty: Không tìm thấy bản ghi.
|
||||||
filter_by_action: Theo hành động
|
filter_by_action: Theo hành động
|
||||||
@ -328,7 +328,7 @@ vi:
|
|||||||
emoji: Emoji
|
emoji: Emoji
|
||||||
enable: Cho phép
|
enable: Cho phép
|
||||||
enabled: Đã 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}
|
image_hint: PNG hoặc GIF tối đa %{size}
|
||||||
list: Danh sách
|
list: Danh sách
|
||||||
listed: Liệt kê
|
listed: Liệt kê
|
||||||
@ -692,7 +692,7 @@ vi:
|
|||||||
manage_announcements: Quản lý thông báo
|
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_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: 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: 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_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
|
manage_custom_emojis: Quản lý emoji
|
||||||
@ -704,7 +704,7 @@ vi:
|
|||||||
manage_reports: Quản lý báo cáo
|
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_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: 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: Quản lý nội quy máy chủ
|
||||||
manage_rules_description: Cho phép thay đổi 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
|
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
|
patch: Bản vá - sửa lỗi và dễ dàng áp dụng các thay đổi
|
||||||
version: Phiên bản
|
version: Phiên bản
|
||||||
statuses:
|
statuses:
|
||||||
account: Tác giả
|
account: Người đăng
|
||||||
application: Ứng dụng
|
application: Ứng dụng
|
||||||
back_to_account: Quay lại trang tài khoản
|
back_to_account: Quay lại trang tài khoản
|
||||||
back_to_report: Quay lại trang báo cáo
|
back_to_report: Quay lại trang báo cáo
|
||||||
@ -817,7 +817,7 @@ vi:
|
|||||||
open: Mở tút
|
open: Mở tút
|
||||||
original_status: Tút gốc
|
original_status: Tút gốc
|
||||||
reblogs: Lượt đăng lại
|
reblogs: Lượt đăng lại
|
||||||
status_changed: Tút đã thay đổi
|
status_changed: Tút đã sửa
|
||||||
title: Toàn bộ tút
|
title: Toàn bộ tút
|
||||||
trending: Xu hướng
|
trending: Xu hướng
|
||||||
visibility: Hiển thị
|
visibility: Hiển thị
|
||||||
@ -896,7 +896,7 @@ vi:
|
|||||||
title: Quản trị
|
title: Quản trị
|
||||||
trends:
|
trends:
|
||||||
allow: Cho phép
|
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_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?
|
confirm_disallow: Bạn có chắc muốn cấm những hashtag đã chọn?
|
||||||
disallow: Cấm
|
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
|
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:
|
shared_by_over_week:
|
||||||
other: "%{count} người chia sẻ tuần rồi"
|
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
|
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
|
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ờ
|
pending_review: Đang chờ
|
||||||
preview_card_providers:
|
preview_card_providers:
|
||||||
allowed: Tin tức từ nguồn này có thể lên xu hướng
|
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ụ.
|
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
|
rejected: Tin tức từ nguồn này không thể lên xu hướng
|
||||||
title: Nguồn đăng
|
title: Nguồn đăng
|
||||||
rejected: Đã cấm
|
rejected: Từ chối
|
||||||
statuses:
|
statuses:
|
||||||
allow: Cho phép tút
|
allow: Cho phép tút
|
||||||
allow_account: Cho phép người đăng
|
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.
|
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: Cấm tút
|
||||||
disallow_account: Cấm người đăng
|
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
|
no_status_selected: Bạn chưa chọn mục nào
|
||||||
not_discoverable: Tác giả đã chọn không tham gia mục khám phá
|
not_discoverable: Người đăng đã chọn không tham gia mục khám phá
|
||||||
shared_by:
|
shared_by:
|
||||||
other: Được thích và đăng lại %{friendly_count} lần
|
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:
|
tags:
|
||||||
current_score: Chỉ số gần đây %{score}
|
current_score: Chỉ số gần đây %{score}
|
||||||
dashboard:
|
dashboard:
|
||||||
@ -956,9 +956,9 @@ vi:
|
|||||||
not_trendable: Không cho lên xu hướng
|
not_trendable: Không cho lên xu hướng
|
||||||
not_usable: Không được phép dùng
|
not_usable: Không được phép dùng
|
||||||
peaked_on_and_decaying: Đỉnh điểm %{date}, giờ đang giảm
|
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
|
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
|
usable: Có thể dùng
|
||||||
usage_comparison: Dùng %{today} lần hôm nay, so với %{yesterday} hôm qua
|
usage_comparison: Dùng %{today} lần hôm nay, so với %{yesterday} hôm qua
|
||||||
used_by_over_week:
|
used_by_over_week:
|
||||||
@ -1004,7 +1004,7 @@ vi:
|
|||||||
silence: hạn chế tài khoản của họ
|
silence: hạn chế tài khoản của họ
|
||||||
suspend: vô hiệu hóa 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:"
|
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}"
|
subject: "%{username} đang khiếu nại quyết định kiểm duyệt trên %{instance}"
|
||||||
new_critical_software_updates:
|
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!
|
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:
|
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:'
|
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:
|
new_trending_links:
|
||||||
title: Tin tức nổi bật
|
title: Xu hướng tin tức
|
||||||
new_trending_statuses:
|
new_trending_statuses:
|
||||||
title: Tút nổi bật
|
title: Xu hướng tút
|
||||||
new_trending_tags:
|
new_trending_tags:
|
||||||
title: Hashtag nổi bật
|
title: Xu hướng hashtag
|
||||||
subject: Nội dung nổi bật chờ duyệt trên %{instance}
|
subject: Xu hướng chờ duyệt trên %{instance}
|
||||||
aliases:
|
aliases:
|
||||||
add_new: Kết nối tài khoản
|
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ũ.
|
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.
|
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}
|
more_from_html: Thêm từ %{name}
|
||||||
s_blog: "%{name}'s Blog"
|
s_blog: "%{name}'s Blog"
|
||||||
title: Ghi nhận tác giả
|
title: Ghi nhận người đăng
|
||||||
challenge:
|
challenge:
|
||||||
confirm: Tiếp tục
|
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."
|
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.
|
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:
|
appeals:
|
||||||
submit: Gửi khiếu nại
|
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
|
associated_report: Báo cáo đính kèm
|
||||||
created_at: Ngày
|
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}.
|
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.
|
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ó
|
invalid_context: Bối cảnh không hợp lệ hoặc không có
|
||||||
index:
|
index:
|
||||||
contexts: Bộ lọc %{contexts}
|
contexts: Lọc ở %{contexts}
|
||||||
delete: Xóa bỏ
|
delete: Xóa bỏ
|
||||||
empty: Chưa có bộ lọc nào.
|
empty: Chưa có bộ lọc nào.
|
||||||
expires_in: Hết hạn trong %{distance}
|
expires_in: Hết hạn trong %{distance}
|
||||||
@ -1336,7 +1336,7 @@ vi:
|
|||||||
merge: Hợp nhất
|
merge: Hợp nhất
|
||||||
merge_long: Giữ hồ sơ hiện có và thêm hồ sơ mới
|
merge_long: Giữ hồ sơ hiện có và thêm hồ sơ mới
|
||||||
overwrite: Ghi đè
|
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:
|
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>.
|
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>.
|
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.
|
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
|
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})
|
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
|
title: Lịch sử đăng nhập
|
||||||
mail_subscriptions:
|
mail_subscriptions:
|
||||||
unsubscribe:
|
unsubscribe:
|
||||||
@ -1832,14 +1832,14 @@ vi:
|
|||||||
spam: Spam
|
spam: Spam
|
||||||
violation: Nội dung vi phạm quy tắc cộng đồng
|
violation: Nội dung vi phạm quy tắc cộng đồng
|
||||||
explanation:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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:'
|
reason: 'Lý do:'
|
||||||
statuses: 'Tút lưu ý:'
|
statuses: 'Tút vi phạm:'
|
||||||
subject:
|
subject:
|
||||||
delete_statuses: Những tút %{acct} của bạn đã bị xóa bỏ
|
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
|
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
|
||||||
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
|
namespace :v2 do
|
||||||
get '/search', to: 'search#index', as: :search
|
get '/search', to: 'search#index', as: :search
|
||||||
|
|
||||||
@ -342,11 +327,18 @@ namespace :api, format: false do
|
|||||||
resource :policy, only: [:show, :update]
|
resource :policy, only: [:show, :update]
|
||||||
end
|
end
|
||||||
|
|
||||||
concerns :grouped_notifications
|
resources :notifications, param: :group_key, only: [:index, :show] do
|
||||||
|
collection do
|
||||||
|
post :clear
|
||||||
|
get :unread_count
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :v2_alpha, module: 'v2' do
|
member do
|
||||||
concerns :grouped_notifications
|
post :dismiss
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :accounts, only: [:index], module: :notifications
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :web do
|
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' }
|
get :new, params: { client_id: app.uid, response_type: 'code', redirect_uri: 'http://localhost/', scope: 'read' }
|
||||||
end
|
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
|
context 'when signed in' do
|
||||||
let!(:user) { Fabricate(:user) }
|
let!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
@ -24,18 +17,17 @@ RSpec.describe Oauth::AuthorizationsController do
|
|||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success and private cache control headers' do
|
||||||
subject
|
subject
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private cache control headers' do
|
expect(response)
|
||||||
subject
|
.to have_http_status(200)
|
||||||
expect(response.headers['Cache-Control']).to include('private, no-store')
|
expect(response.headers['Cache-Control'])
|
||||||
|
.to include('private, no-store')
|
||||||
|
expect(controller.stored_location_for(:user))
|
||||||
|
.to eq authorize_path_for(app)
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'stores location for user'
|
|
||||||
|
|
||||||
context 'when app is already authorized' do
|
context 'when app is already authorized' do
|
||||||
before do
|
before do
|
||||||
Doorkeeper::AccessToken.find_or_create_for(
|
Doorkeeper::AccessToken.find_or_create_for(
|
||||||
@ -52,10 +44,12 @@ RSpec.describe Oauth::AuthorizationsController do
|
|||||||
expect(response).to redirect_to(/\A#{app.redirect_uri}/)
|
expect(response).to redirect_to(/\A#{app.redirect_uri}/)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not redirect to callback with force_login=true' do
|
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' }
|
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
|
end
|
||||||
end
|
end
|
||||||
@ -63,10 +57,16 @@ RSpec.describe Oauth::AuthorizationsController do
|
|||||||
context 'when not signed in' do
|
context 'when not signed in' do
|
||||||
it 'redirects' do
|
it 'redirects' do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to '/auth/sign_in'
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to '/auth/sign_in'
|
||||||
|
expect(controller.stored_location_for(:user))
|
||||||
|
.to eq authorize_path_for(app)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'stores location for user'
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,38 +10,31 @@ RSpec.describe Oauth::AuthorizedApplicationsController do
|
|||||||
get :index
|
get :index
|
||||||
end
|
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
|
context 'when signed in' do
|
||||||
before do
|
before do
|
||||||
sign_in Fabricate(:user), scope: :user
|
sign_in Fabricate(:user), scope: :user
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success with private cache control headers' do
|
||||||
subject
|
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
|
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
|
end
|
||||||
|
|
||||||
context 'when not signed in' do
|
context 'when not signed in' do
|
||||||
it 'redirects' do
|
it 'redirects' do
|
||||||
subject
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -55,23 +48,19 @@ RSpec.describe Oauth::AuthorizedApplicationsController do
|
|||||||
before do
|
before do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
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 }
|
post :destroy, params: { id: application.id }
|
||||||
end
|
|
||||||
|
|
||||||
it 'revokes access tokens for the application' do
|
expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at)
|
||||||
expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at).to_not be_nil
|
.to_not be_nil
|
||||||
end
|
expect(Web::PushSubscription.where(user: user).count)
|
||||||
|
.to eq(0)
|
||||||
it 'removes subscriptions for the application\'s access tokens' do
|
expect { web_push_subscription.reload }
|
||||||
expect(Web::PushSubscription.where(user: user).count).to eq 0
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
expect(redis_pipeline_stub)
|
||||||
|
.to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
|
||||||
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"}')
|
|
||||||
end
|
end
|
||||||
end
|
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!(: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) }
|
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 }
|
post :revoke, params: { client_id: application.uid, token: access_token.token }
|
||||||
end
|
|
||||||
|
|
||||||
it 'revokes the token' do
|
expect(access_token.reload.revoked_at)
|
||||||
expect(access_token.reload.revoked_at).to_not be_nil
|
.to_not be_nil
|
||||||
end
|
expect(Web::PushSubscription.where(access_token: access_token).count)
|
||||||
|
.to eq(0)
|
||||||
it 'removes web push subscription for token' do
|
expect { web_push_subscription.reload }
|
||||||
expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes the web_push_subscription' do
|
|
||||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,16 +5,10 @@ require 'rails_helper'
|
|||||||
RSpec.describe Settings::FeaturedTagsController do
|
RSpec.describe Settings::FeaturedTagsController do
|
||||||
render_views
|
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
|
context 'when user is not signed in' do
|
||||||
subject { post :create }
|
subject { post :create }
|
||||||
|
|
||||||
it_behaves_like 'authenticate user'
|
it { is_expected.to redirect_to new_user_session_path }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is signed in' do
|
context 'when user is signed in' do
|
||||||
|
@ -5,17 +5,11 @@ require 'rails_helper'
|
|||||||
RSpec.describe Settings::MigrationsController do
|
RSpec.describe Settings::MigrationsController do
|
||||||
render_views
|
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
|
describe 'GET #show' do
|
||||||
context 'when user is not sign in' do
|
context 'when user is not sign in' do
|
||||||
subject { get :show }
|
subject { get :show }
|
||||||
|
|
||||||
it_behaves_like 'authenticate user'
|
it { is_expected.to redirect_to new_user_session_path }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is sign in' do
|
context 'when user is sign in' do
|
||||||
@ -49,7 +43,7 @@ RSpec.describe Settings::MigrationsController do
|
|||||||
context 'when user is not sign in' do
|
context 'when user is not sign in' do
|
||||||
subject { post :create }
|
subject { post :create }
|
||||||
|
|
||||||
it_behaves_like 'authenticate user'
|
it { is_expected.to redirect_to new_user_session_path }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is signed in' do
|
context 'when user is signed in' do
|
||||||
|
@ -29,5 +29,20 @@ RSpec.describe PermalinkRedirector do
|
|||||||
redirector = described_class.new('@alice/123')
|
redirector = described_class.new('@alice/123')
|
||||||
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
|
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -7,16 +7,13 @@ RSpec.describe ScopeTransformer do
|
|||||||
subject { described_class.new.apply(ScopeParser.new.parse(input)) }
|
subject { described_class.new.apply(ScopeParser.new.parse(input)) }
|
||||||
|
|
||||||
shared_examples 'a scope' do |namespace, term, access|
|
shared_examples 'a scope' do |namespace, term, access|
|
||||||
it 'parses the term' do
|
it 'parses the attributes' do
|
||||||
expect(subject.term).to eq term
|
expect(subject)
|
||||||
end
|
.to have_attributes(
|
||||||
|
term: term,
|
||||||
it 'parses the namespace' do
|
namespace: namespace,
|
||||||
expect(subject.namespace).to eq namespace
|
access: access
|
||||||
end
|
)
|
||||||
|
|
||||||
it 'parses the access' do
|
|
||||||
expect(subject.access).to eq access
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,6 +3,17 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe NotificationMailer do
|
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(:receiver) { Fabricate(:user, account_attributes: { username: 'alice' }) }
|
||||||
let(:sender) { Fabricate(:account, username: 'bob') }
|
let(:sender) { Fabricate(:account, username: 'bob') }
|
||||||
let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }
|
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_thread_headers
|
||||||
.and have_standard_headers('mention').for(receiver)
|
.and have_standard_headers('mention').for(receiver)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'delivery to non functional user'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'follow' do
|
describe 'follow' do
|
||||||
@ -40,6 +53,8 @@ RSpec.describe NotificationMailer do
|
|||||||
.and(have_body_text('bob is now following you'))
|
.and(have_body_text('bob is now following you'))
|
||||||
.and have_standard_headers('follow').for(receiver)
|
.and have_standard_headers('follow').for(receiver)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'delivery to non functional user'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'favourite' do
|
describe 'favourite' do
|
||||||
@ -58,6 +73,8 @@ RSpec.describe NotificationMailer do
|
|||||||
.and have_thread_headers
|
.and have_thread_headers
|
||||||
.and have_standard_headers('favourite').for(receiver)
|
.and have_standard_headers('favourite').for(receiver)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'delivery to non functional user'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'reblog' do
|
describe 'reblog' do
|
||||||
@ -76,6 +93,8 @@ RSpec.describe NotificationMailer do
|
|||||||
.and have_thread_headers
|
.and have_thread_headers
|
||||||
.and have_standard_headers('reblog').for(receiver)
|
.and have_standard_headers('reblog').for(receiver)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'delivery to non functional user'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'follow_request' do
|
describe 'follow_request' do
|
||||||
@ -92,6 +111,8 @@ RSpec.describe NotificationMailer do
|
|||||||
.and(have_body_text('bob has requested to follow you'))
|
.and(have_body_text('bob has requested to follow you'))
|
||||||
.and have_standard_headers('follow_request').for(receiver)
|
.and have_standard_headers('follow_request').for(receiver)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'delivery to non functional user'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -23,8 +23,11 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq 2
|
expect(response.parsed_body)
|
||||||
expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s),
|
||||||
|
hash_including(id: bob.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not return blocked users', :aggregate_failures do
|
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).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq 1
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body[0][:id]).to eq alice.id.to_s
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when requesting user is blocked' do
|
context 'when requesting user is blocked' do
|
||||||
@ -56,8 +61,11 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
|
|||||||
account.mute!(bob)
|
account.mute!(bob)
|
||||||
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
|
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
|
||||||
|
|
||||||
expect(response.parsed_body.size).to eq 2
|
expect(response.parsed_body)
|
||||||
expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s),
|
||||||
|
hash_including(id: bob.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -23,8 +23,11 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq 2
|
expect(response.parsed_body)
|
||||||
expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s),
|
||||||
|
hash_including(id: bob.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not return blocked users', :aggregate_failures do
|
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).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq 1
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body[0][:id]).to eq alice.id.to_s
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when requesting user is blocked' do
|
context 'when requesting user is blocked' do
|
||||||
@ -56,8 +61,11 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
|
|||||||
account.mute!(bob)
|
account.mute!(bob)
|
||||||
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
|
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
|
||||||
|
|
||||||
expect(response.parsed_body.size).to eq 2
|
expect(response.parsed_body)
|
||||||
expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
|
.to contain_exactly(
|
||||||
|
hash_including(id: alice.id.to_s),
|
||||||
|
hash_including(id: bob.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -84,8 +84,11 @@ RSpec.describe 'Directories API' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq(2)
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body.pluck(:id)).to contain_exactly(eligible_remote_account.id.to_s, local_discoverable_account.id.to_s)
|
.to contain_exactly(
|
||||||
|
hash_including(id: eligible_remote_account.id.to_s),
|
||||||
|
hash_including(id: local_discoverable_account.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -105,9 +108,11 @@ RSpec.describe 'Directories API' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq(1)
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body.first[:id]).to include(local_account.id.to_s)
|
.to contain_exactly(
|
||||||
expect(response.body).to_not include(remote_account.id.to_s)
|
hash_including(id: local_account.id.to_s)
|
||||||
|
)
|
||||||
|
.and not_include(remote_account.id.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -121,9 +126,11 @@ RSpec.describe 'Directories API' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq(2)
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body.first[:id]).to include(new_stat.account_id.to_s)
|
.to contain_exactly(
|
||||||
expect(response.parsed_body.second[:id]).to include(old_stat.account_id.to_s)
|
hash_including(id: new_stat.account_id.to_s),
|
||||||
|
hash_including(id: old_stat.account_id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -138,9 +145,11 @@ RSpec.describe 'Directories API' do
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size).to eq(2)
|
expect(response.parsed_body)
|
||||||
expect(response.parsed_body.first[:id]).to include(account_new.id.to_s)
|
.to contain_exactly(
|
||||||
expect(response.parsed_body.second[:id]).to include(account_old.id.to_s)
|
hash_including(id: account_new.id.to_s),
|
||||||
|
hash_including(id: account_old.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -55,10 +55,10 @@ RSpec.describe 'API Peers Search' do
|
|||||||
.to have_http_status(200)
|
.to have_http_status(200)
|
||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
expect(response.parsed_body.size)
|
expect(response.parsed_body)
|
||||||
.to eq(1)
|
.to contain_exactly(
|
||||||
expect(response.parsed_body.first)
|
eq(account.domain)
|
||||||
.to eq(account.domain)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -36,8 +36,6 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do
|
|||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
|
|
||||||
expect(response.parsed_body.size)
|
|
||||||
.to eq(2)
|
|
||||||
expect(response.parsed_body)
|
expect(response.parsed_body)
|
||||||
.to contain_exactly(
|
.to contain_exactly(
|
||||||
include(id: alice.id.to_s),
|
include(id: alice.id.to_s),
|
||||||
@ -50,9 +48,10 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do
|
|||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response.parsed_body.size)
|
expect(response.parsed_body)
|
||||||
.to eq 1
|
.to contain_exactly(
|
||||||
expect(response.parsed_body.first[:id]).to eq(alice.id.to_s)
|
hash_including(id: alice.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -35,8 +35,6 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do
|
|||||||
expect(response.content_type)
|
expect(response.content_type)
|
||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
|
|
||||||
expect(response.parsed_body.size)
|
|
||||||
.to eq(2)
|
|
||||||
expect(response.parsed_body)
|
expect(response.parsed_body)
|
||||||
.to contain_exactly(
|
.to contain_exactly(
|
||||||
include(id: alice.id.to_s),
|
include(id: alice.id.to_s),
|
||||||
@ -49,9 +47,10 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do
|
|||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response.parsed_body.size)
|
expect(response.parsed_body)
|
||||||
.to eq 1
|
.to contain_exactly(
|
||||||
expect(response.parsed_body.first[:id]).to eq(alice.id.to_s)
|
hash_including(id: alice.id.to_s)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
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