@@ -7,14 +7,17 @@ class AccountsController < ApplicationController | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
@pinned_statuses = [] | |||
if current_account && @account.blocking?(current_account) | |||
@statuses = [] | |||
return | |||
end | |||
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) | |||
@statuses = cache_collection(@statuses, Status) | |||
@next_url = next_url unless @statuses.empty? | |||
@pinned_statuses = cache_collection(@account.pinned_statuses.limit(1), Status) unless media_requested? | |||
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) | |||
@statuses = cache_collection(@statuses, Status) | |||
@next_url = next_url unless @statuses.empty? | |||
end | |||
format.atom do | |||
@@ -32,8 +35,8 @@ class AccountsController < ApplicationController | |||
def filtered_statuses | |||
default_statuses.tap do |statuses| | |||
statuses.merge!(only_media_scope) if request.path.ends_with?('/media') | |||
statuses.merge!(no_replies_scope) unless request.path.ends_with?('/with_replies') | |||
statuses.merge!(only_media_scope) if media_requested? | |||
statuses.merge!(no_replies_scope) unless replies_requested? | |||
end | |||
end | |||
@@ -58,12 +61,20 @@ class AccountsController < ApplicationController | |||
end | |||
def next_url | |||
if request.path.ends_with?('/media') | |||
if media_requested? | |||
short_account_media_url(@account, max_id: @statuses.last.id) | |||
elsif request.path.ends_with?('/with_replies') | |||
elsif replies_requested? | |||
short_account_with_replies_url(@account, max_id: @statuses.last.id) | |||
else | |||
short_account_url(@account, max_id: @statuses.last.id) | |||
end | |||
end | |||
def media_requested? | |||
request.path.ends_with?('/media') | |||
end | |||
def replies_requested? | |||
request.path.ends_with?('/with_replies') | |||
end | |||
end |
@@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | |||
def account_statuses | |||
default_statuses.tap do |statuses| | |||
statuses.merge!(only_media_scope) if params[:only_media] | |||
statuses.merge!(pinned_scope) if params[:pinned] | |||
statuses.merge!(no_replies_scope) if params[:exclude_replies] | |||
end | |||
end | |||
@@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | |||
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct | |||
end | |||
def pinned_scope | |||
@account.pinned_statuses | |||
end | |||
def no_replies_scope | |||
Status.without_replies | |||
end | |||
@@ -0,0 +1,28 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Statuses::PinsController < Api::BaseController | |||
include Authorization | |||
before_action -> { doorkeeper_authorize! :write } | |||
before_action :require_user! | |||
before_action :set_status | |||
respond_to :json | |||
def create | |||
StatusPin.create!(account: current_account, status: @status) | |||
render json: @status, serializer: REST::StatusSerializer | |||
end | |||
def destroy | |||
pin = StatusPin.find_by(account: current_account, status: @status) | |||
pin&.destroy! | |||
render json: @status, serializer: REST::StatusSerializer | |||
end | |||
private | |||
def set_status | |||
@status = Status.find(params[:status_id]) | |||
end | |||
end |
@@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; | |||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; | |||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; | |||
export const PIN_REQUEST = 'PIN_REQUEST'; | |||
export const PIN_SUCCESS = 'PIN_SUCCESS'; | |||
export const PIN_FAIL = 'PIN_FAIL'; | |||
export const UNPIN_REQUEST = 'UNPIN_REQUEST'; | |||
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; | |||
export const UNPIN_FAIL = 'UNPIN_FAIL'; | |||
export function reblog(status) { | |||
return function (dispatch, getState) { | |||
dispatch(reblogRequest(status)); | |||
@@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) { | |||
error, | |||
}; | |||
}; | |||
export function pin(status) { | |||
return (dispatch, getState) => { | |||
dispatch(pinRequest(status)); | |||
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { | |||
dispatch(pinSuccess(status, response.data)); | |||
}).catch(error => { | |||
dispatch(pinFail(status, error)); | |||
}); | |||
}; | |||
}; | |||
export function pinRequest(status) { | |||
return { | |||
type: PIN_REQUEST, | |||
status, | |||
}; | |||
}; | |||
export function pinSuccess(status, response) { | |||
return { | |||
type: PIN_SUCCESS, | |||
status, | |||
response, | |||
}; | |||
}; | |||
export function pinFail(status, error) { | |||
return { | |||
type: PIN_FAIL, | |||
status, | |||
error, | |||
}; | |||
}; | |||
export function unpin (status) { | |||
return (dispatch, getState) => { | |||
dispatch(unpinRequest(status)); | |||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { | |||
dispatch(unpinSuccess(status, response.data)); | |||
}).catch(error => { | |||
dispatch(unpinFail(status, error)); | |||
}); | |||
}; | |||
}; | |||
export function unpinRequest(status) { | |||
return { | |||
type: UNPIN_REQUEST, | |||
status, | |||
}; | |||
}; | |||
export function unpinSuccess(status, response) { | |||
return { | |||
type: UNPIN_SUCCESS, | |||
status, | |||
response, | |||
}; | |||
}; | |||
export function unpinFail(status, error) { | |||
return { | |||
type: UNPIN_FAIL, | |||
status, | |||
error, | |||
}; | |||
}; |
@@ -31,6 +31,7 @@ export default class Status extends ImmutablePureComponent { | |||
onFavourite: PropTypes.func, | |||
onReblog: PropTypes.func, | |||
onDelete: PropTypes.func, | |||
onPin: PropTypes.func, | |||
onOpenMedia: PropTypes.func, | |||
onOpenVideo: PropTypes.func, | |||
onBlock: PropTypes.func, | |||
@@ -21,6 +21,8 @@ const messages = defineMessages({ | |||
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, | |||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, | |||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, | |||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, | |||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, | |||
}); | |||
@injectIntl | |||
@@ -41,6 +43,7 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
onBlock: PropTypes.func, | |||
onReport: PropTypes.func, | |||
onMuteConversation: PropTypes.func, | |||
onPin: PropTypes.func, | |||
me: PropTypes.number, | |||
withDismiss: PropTypes.bool, | |||
intl: PropTypes.object.isRequired, | |||
@@ -77,6 +80,10 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
this.props.onDelete(this.props.status); | |||
} | |||
handlePinClick = () => { | |||
this.props.onPin(this.props.status); | |||
} | |||
handleMentionClick = () => { | |||
this.props.onMention(this.props.status.get('account'), this.context.router.history); | |||
} | |||
@@ -121,6 +128,10 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
} | |||
if (status.getIn(['account', 'id']) === me) { | |||
if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { | |||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); | |||
} | |||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); | |||
} else { | |||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); | |||
@@ -11,6 +11,8 @@ import { | |||
favourite, | |||
unreblog, | |||
unfavourite, | |||
pin, | |||
unpin, | |||
} from '../actions/interactions'; | |||
import { | |||
blockAccount, | |||
@@ -72,6 +74,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | |||
} | |||
}, | |||
onPin (status) { | |||
if (status.get('pinned')) { | |||
dispatch(unpin(status)); | |||
} else { | |||
dispatch(pin(status)); | |||
} | |||
}, | |||
onDelete (status) { | |||
if (!this.deleteModal) { | |||
dispatch(deleteStatus(status.get('id'))); | |||
@@ -14,6 +14,8 @@ const messages = defineMessages({ | |||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, | |||
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, | |||
share: { id: 'status.share', defaultMessage: 'Share' }, | |||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, | |||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, | |||
}); | |||
@injectIntl | |||
@@ -31,6 +33,7 @@ export default class ActionBar extends React.PureComponent { | |||
onDelete: PropTypes.func.isRequired, | |||
onMention: PropTypes.func.isRequired, | |||
onReport: PropTypes.func, | |||
onPin: PropTypes.func, | |||
me: PropTypes.number.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
}; | |||
@@ -59,6 +62,10 @@ export default class ActionBar extends React.PureComponent { | |||
this.props.onReport(this.props.status); | |||
} | |||
handlePinClick = () => { | |||
this.props.onPin(this.props.status); | |||
} | |||
handleShare = () => { | |||
navigator.share({ | |||
text: this.props.status.get('search_index'), | |||
@@ -72,6 +79,10 @@ export default class ActionBar extends React.PureComponent { | |||
let menu = []; | |||
if (me === status.getIn(['account', 'id'])) { | |||
if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { | |||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); | |||
} | |||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); | |||
} else { | |||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); | |||
@@ -12,6 +12,8 @@ import { | |||
unfavourite, | |||
reblog, | |||
unreblog, | |||
pin, | |||
unpin, | |||
} from '../../actions/interactions'; | |||
import { | |||
replyCompose, | |||
@@ -87,6 +89,14 @@ export default class Status extends ImmutablePureComponent { | |||
} | |||
} | |||
handlePin = (status) => { | |||
if (status.get('pinned')) { | |||
this.props.dispatch(unpin(status)); | |||
} else { | |||
this.props.dispatch(pin(status)); | |||
} | |||
} | |||
handleReplyClick = (status) => { | |||
this.props.dispatch(replyCompose(status, this.context.router.history)); | |||
} | |||
@@ -187,6 +197,7 @@ export default class Status extends ImmutablePureComponent { | |||
onDelete={this.handleDeleteClick} | |||
onMention={this.handleMentionClick} | |||
onReport={this.handleReport} | |||
onPin={this.handlePin} | |||
/> | |||
{descendants} | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "أذكُر @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "وسع هذه المشاركة", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "رَقِّي", | |||
"status.reblogged_by": "{name} رقى", | |||
"status.reply": "ردّ", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "إعرض أقلّ", | |||
"status.show_more": "أظهر المزيد", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "تحرير", | |||
"tabs_bar.federated_timeline": "الموحَّد", | |||
"tabs_bar.home": "الرئيسية", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Споменаване", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Споделяне", | |||
"status.reblogged_by": "{name} сподели", | |||
"status.reply": "Отговор", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Съставяне", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Начало", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Esmentar @{name}", | |||
"status.mute_conversation": "Silenciar conversació", | |||
"status.open": "Ampliar aquest estat", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost", | |||
"status.reblogged_by": "{name} ha retootejat", | |||
"status.reply": "Respondre", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Mostra menys", | |||
"status.show_more": "Mostra més", | |||
"status.unmute_conversation": "Activar conversació", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Compondre", | |||
"tabs_bar.federated_timeline": "Federada", | |||
"tabs_bar.home": "Inici", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Erwähnen", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Öffnen", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Teilen", | |||
"status.reblogged_by": "{name} teilte", | |||
"status.reply": "Antworten", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Weniger anzeigen", | |||
"status.show_more": "Mehr anzeigen", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Schreiben", | |||
"tabs_bar.federated_timeline": "Föderation", | |||
"tabs_bar.home": "Home", | |||
@@ -189,6 +189,14 @@ | |||
{ | |||
"defaultMessage": "Unmute conversation", | |||
"id": "status.unmute_conversation" | |||
}, | |||
{ | |||
"defaultMessage": "Pin on profile", | |||
"id": "status.pin" | |||
}, | |||
{ | |||
"defaultMessage": "Unpin from profile", | |||
"id": "status.unpin" | |||
} | |||
], | |||
"path": "app/javascript/mastodon/components/status_action_bar.json" | |||
@@ -1035,6 +1043,14 @@ | |||
{ | |||
"defaultMessage": "Share", | |||
"id": "status.share" | |||
}, | |||
{ | |||
"defaultMessage": "Pin on profile", | |||
"id": "status.pin" | |||
}, | |||
{ | |||
"defaultMessage": "Unpin from profile", | |||
"id": "status.unpin" | |||
} | |||
], | |||
"path": "app/javascript/mastodon/features/status/components/action_bar.json" | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mention @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost", | |||
"status.reblogged_by": "{name} boosted", | |||
"status.reply": "Reply", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Compose", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Home", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencii @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Diskonigi", | |||
"status.reblogged_by": "{name} diskonigita", | |||
"status.reply": "Respondi", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Ekskribi", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Hejmo", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencionar", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expandir estado", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Retoot", | |||
"status.reblogged_by": "Retooteado por {name}", | |||
"status.reply": "Responder", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Mostrar menos", | |||
"status.show_more": "Mostrar más", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Redactar", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Inicio", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "نامبردن از @{name}", | |||
"status.mute_conversation": "بیصداکردن گفتگو", | |||
"status.open": "این نوشته را باز کن", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "بازبوقیدن", | |||
"status.reblogged_by": "{name} بازبوقید", | |||
"status.reply": "پاسخ", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "نهفتن", | |||
"status.show_more": "نمایش", | |||
"status.unmute_conversation": "باصداکردن گفتگو", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "بنویسید", | |||
"tabs_bar.federated_timeline": "همگانی", | |||
"tabs_bar.home": "خانه", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mainitse @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Buustaa", | |||
"status.reblogged_by": "{name} buustasi", | |||
"status.reply": "Vastaa", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Luo", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Koti", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mentionner", | |||
"status.mute_conversation": "Masquer la conversation", | |||
"status.open": "Déplier ce statut", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Partager", | |||
"status.reblogged_by": "{name} a partagé :", | |||
"status.reply": "Répondre", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Replier", | |||
"status.show_more": "Déplier", | |||
"status.unmute_conversation": "Ne plus masquer la conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Composer", | |||
"tabs_bar.federated_timeline": "Fil public global", | |||
"tabs_bar.home": "Accueil", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "פניה אל @{name}", | |||
"status.mute_conversation": "השתקת שיחה", | |||
"status.open": "הרחבת הודעה", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "הדהוד", | |||
"status.reblogged_by": "הודהד על ידי {name}", | |||
"status.reply": "תגובה", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "הראה פחות", | |||
"status.show_more": "הראה יותר", | |||
"status.unmute_conversation": "הסרת השתקת שיחה", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "חיבור", | |||
"tabs_bar.federated_timeline": "ציר זמן בין-קהילתי", | |||
"tabs_bar.home": "בבית", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Spomeni @{name}", | |||
"status.mute_conversation": "Utišaj razgovor", | |||
"status.open": "Proširi ovaj status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Podigni", | |||
"status.reblogged_by": "{name} je podigao", | |||
"status.reply": "Odgovori", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Pokaži manje", | |||
"status.show_more": "Pokaži više", | |||
"status.unmute_conversation": "Poništi utišavanje razgovora", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Sastavi", | |||
"tabs_bar.federated_timeline": "Federalni", | |||
"tabs_bar.home": "Dom", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Említés", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Reblog", | |||
"status.reblogged_by": "{name} reblogolta", | |||
"status.reply": "Válasz", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Összeállítás", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Kezdőlap", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Balasan @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Tampilkan status ini", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost", | |||
"status.reblogged_by": "di-boost {name}", | |||
"status.reply": "Balas", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Tampilkan lebih sedikit", | |||
"status.show_more": "Tampilkan semua", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Tulis", | |||
"tabs_bar.federated_timeline": "Gabungan", | |||
"tabs_bar.home": "Beranda", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencionar @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Detaligar ca mesajo", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Repetar", | |||
"status.reblogged_by": "{name} repetita", | |||
"status.reply": "Respondar", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Montrar mine", | |||
"status.show_more": "Montrar plue", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Kompozar", | |||
"tabs_bar.federated_timeline": "Federata", | |||
"tabs_bar.home": "Hemo", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Nomina @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Espandi questo post", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Condividi", | |||
"status.reblogged_by": "{name} ha condiviso", | |||
"status.reply": "Rispondi", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Mostra meno", | |||
"status.show_more": "Mostra di più", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Scrivi", | |||
"tabs_bar.federated_timeline": "Federazione", | |||
"tabs_bar.home": "Home", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "返信", | |||
"status.mute_conversation": "会話をミュート", | |||
"status.open": "詳細を表示", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "ブースト", | |||
"status.reblogged_by": "{name}さんにブーストされました", | |||
"status.reply": "返信", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "隠す", | |||
"status.show_more": "もっと見る", | |||
"status.unmute_conversation": "会話のミュートを解除", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "投稿", | |||
"tabs_bar.federated_timeline": "連合", | |||
"tabs_bar.home": "ホーム", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "답장", | |||
"status.mute_conversation": "이 대화를 뮤트", | |||
"status.open": "상세 정보 표시", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "부스트", | |||
"status.reblogged_by": "{name}님이 부스트 했습니다", | |||
"status.reply": "답장", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "숨기기", | |||
"status.show_more": "더 보기", | |||
"status.unmute_conversation": "이 대화의 뮤트 해제하기", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "포스트", | |||
"tabs_bar.federated_timeline": "연합", | |||
"tabs_bar.home": "홈", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Vermeld @{name}", | |||
"status.mute_conversation": "Negeer conversatie", | |||
"status.open": "Toot volledig tonen", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost", | |||
"status.reblogged_by": "{name} boostte", | |||
"status.reply": "Reageren", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Minder tonen", | |||
"status.show_more": "Meer tonen", | |||
"status.unmute_conversation": "Conversatie niet meer negeren", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Schrijven", | |||
"tabs_bar.federated_timeline": "Globaal", | |||
"tabs_bar.home": "Start", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Nevn @{name}", | |||
"status.mute_conversation": "Demp samtale", | |||
"status.open": "Utvid denne statusen", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Fremhev", | |||
"status.reblogged_by": "Fremhevd av {name}", | |||
"status.reply": "Svar", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Vis mindre", | |||
"status.show_more": "Vis mer", | |||
"status.unmute_conversation": "Ikke demp samtale", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Komponer", | |||
"tabs_bar.federated_timeline": "Felles", | |||
"tabs_bar.home": "Hjem", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencionar", | |||
"status.mute_conversation": "Rescondre la conversacion", | |||
"status.open": "Desplegar aqueste estatut", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Partejar", | |||
"status.reblogged_by": "{name} a partejat :", | |||
"status.reply": "Respondre", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Tornar plegar", | |||
"status.show_more": "Desplegar", | |||
"status.unmute_conversation": "Conversacions amb silenci levat", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Compausar", | |||
"tabs_bar.federated_timeline": "Flux public global", | |||
"tabs_bar.home": "Acuèlh", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Wspomnij o @{name}", | |||
"status.mute_conversation": "Wycisz konwersację", | |||
"status.open": "Rozszerz ten status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Podbij", | |||
"status.reblogged_by": "{name} podbił", | |||
"status.reply": "Odpowiedz", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Pokaż mniej", | |||
"status.show_more": "Pokaż więcej", | |||
"status.unmute_conversation": "Cofnij wyciszenie konwersacji", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Napisz", | |||
"tabs_bar.federated_timeline": "Globalne", | |||
"tabs_bar.home": "Strona główna", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencionar @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expandir", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Partilhar", | |||
"status.reblogged_by": "{name} partilhou", | |||
"status.reply": "Responder", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Mostrar menos", | |||
"status.show_more": "Mostrar mais", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Criar", | |||
"tabs_bar.federated_timeline": "Global", | |||
"tabs_bar.home": "Home", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mencionar @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expandir", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Partilhar", | |||
"status.reblogged_by": "{name} partilhou", | |||
"status.reply": "Responder", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Mostrar menos", | |||
"status.show_more": "Mostrar mais", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Criar", | |||
"tabs_bar.federated_timeline": "Global", | |||
"tabs_bar.home": "Home", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Упомянуть @{name}", | |||
"status.mute_conversation": "Заглушить тред", | |||
"status.open": "Развернуть статус", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Продвинуть", | |||
"status.reblogged_by": "{name} продвинул(а)", | |||
"status.reply": "Ответить", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Свернуть", | |||
"status.show_more": "Развернуть", | |||
"status.unmute_conversation": "Снять глушение с треда", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Написать", | |||
"tabs_bar.federated_timeline": "Глобальная", | |||
"tabs_bar.home": "Главная", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Mention @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Expand this status", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost", | |||
"status.reblogged_by": "{name} boosted", | |||
"status.reply": "Reply", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Show less", | |||
"status.show_more": "Show more", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Compose", | |||
"tabs_bar.federated_timeline": "Federated", | |||
"tabs_bar.home": "Home", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Bahset @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "Bu gönderiyi genişlet", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Boost'la", | |||
"status.reblogged_by": "{name} boost etti", | |||
"status.reply": "Cevapla", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Daha azı", | |||
"status.show_more": "Daha fazlası", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Oluştur", | |||
"tabs_bar.federated_timeline": "Federe", | |||
"tabs_bar.home": "Ana sayfa", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "Згадати", | |||
"status.mute_conversation": "Заглушити діалог", | |||
"status.open": "Розгорнути допис", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "Передмухнути", | |||
"status.reblogged_by": "{name} передмухнув(-ла)", | |||
"status.reply": "Відповісти", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "Згорнути", | |||
"status.show_more": "Розгорнути", | |||
"status.unmute_conversation": "Зняти глушення з діалогу", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "Написати", | |||
"tabs_bar.federated_timeline": "Глобальна", | |||
"tabs_bar.home": "Головна", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "提及 @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "展开嘟文", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "转嘟", | |||
"status.reblogged_by": "{name} 转嘟", | |||
"status.reply": "回应", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "减少显示", | |||
"status.show_more": "显示更多", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "撰写", | |||
"tabs_bar.federated_timeline": "跨站", | |||
"tabs_bar.home": "主页", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "提及 @{name}", | |||
"status.mute_conversation": "Mute conversation", | |||
"status.open": "展開文章", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "轉推", | |||
"status.reblogged_by": "{name} 轉推", | |||
"status.reply": "回應", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "減少顯示", | |||
"status.show_more": "顯示更多", | |||
"status.unmute_conversation": "Unmute conversation", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "撰寫", | |||
"tabs_bar.federated_timeline": "跨站", | |||
"tabs_bar.home": "主頁", | |||
@@ -168,6 +168,7 @@ | |||
"status.mention": "提到 @{name}", | |||
"status.mute_conversation": "消音對話", | |||
"status.open": "展開這個狀態", | |||
"status.pin": "Pin on profile", | |||
"status.reblog": "轉推", | |||
"status.reblogged_by": "{name} 轉推了", | |||
"status.reply": "回應", | |||
@@ -179,6 +180,7 @@ | |||
"status.show_less": "看少點", | |||
"status.show_more": "看更多", | |||
"status.unmute_conversation": "不消音對話", | |||
"status.unpin": "Unpin from profile", | |||
"tabs_bar.compose": "編輯", | |||
"tabs_bar.federated_timeline": "聯盟", | |||
"tabs_bar.home": "家", | |||
@@ -7,6 +7,8 @@ import { | |||
FAVOURITE_SUCCESS, | |||
FAVOURITE_FAIL, | |||
UNFAVOURITE_SUCCESS, | |||
PIN_SUCCESS, | |||
UNPIN_SUCCESS, | |||
} from '../actions/interactions'; | |||
import { | |||
STATUS_FETCH_SUCCESS, | |||
@@ -114,6 +116,8 @@ export default function statuses(state = initialState, action) { | |||
case UNREBLOG_SUCCESS: | |||
case FAVOURITE_SUCCESS: | |||
case UNFAVOURITE_SUCCESS: | |||
case PIN_SUCCESS: | |||
case UNPIN_SUCCESS: | |||
return normalizeStatus(state, action.response); | |||
case FAVOURITE_REQUEST: | |||
return state.setIn([action.status.get('id'), 'favourited'], true); | |||
@@ -77,6 +77,10 @@ class Account < ApplicationRecord | |||
has_many :mentions, inverse_of: :account, dependent: :destroy | |||
has_many :notifications, inverse_of: :account, dependent: :destroy | |||
# Pinned statuses | |||
has_many :status_pins, inverse_of: :account, dependent: :destroy | |||
has_many :pinned_statuses, through: :status_pins, class_name: 'Status', source: :status | |||
# Media | |||
has_many :media_attachments, dependent: :destroy | |||
@@ -138,4 +138,8 @@ module AccountInteractions | |||
def reblogged?(status) | |||
status.proper.reblogs.where(account: self).exists? | |||
end | |||
def pinned?(status) | |||
status_pins.where(status: status).exists? | |||
end | |||
end |
@@ -164,6 +164,10 @@ class Status < ApplicationRecord | |||
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h | |||
end | |||
def pins_map(status_ids, account_id) | |||
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h | |||
end | |||
def reload_stale_associations!(cached_items) | |||
account_ids = [] | |||
@@ -0,0 +1,16 @@ | |||
# frozen_string_literal: true | |||
# == Schema Information | |||
# | |||
# Table name: status_pins | |||
# | |||
# id :integer not null, primary key | |||
# account_id :integer not null | |||
# status_id :integer not null | |||
# | |||
class StatusPin < ApplicationRecord | |||
belongs_to :account, required: true | |||
belongs_to :status, required: true | |||
validates_with StatusPinValidator | |||
end |
@@ -1,19 +1,24 @@ | |||
# frozen_string_literal: true | |||
class StatusRelationshipsPresenter | |||
attr_reader :reblogs_map, :favourites_map, :mutes_map | |||
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map | |||
def initialize(statuses, current_account_id = nil, reblogs_map: {}, favourites_map: {}, mutes_map: {}) | |||
def initialize(statuses, current_account_id = nil, options = {}) | |||
if current_account_id.nil? | |||
@reblogs_map = {} | |||
@favourites_map = {} | |||
@mutes_map = {} | |||
@pins_map = {} | |||
else | |||
status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq | |||
conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq | |||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(reblogs_map) | |||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(favourites_map) | |||
@mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(mutes_map) | |||
statuses = statuses.compact | |||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq | |||
conversation_ids = statuses.map(&:conversation_id).compact.uniq | |||
pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted).include?(s.visibility) }.map(&:id) | |||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) | |||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) | |||
@mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) | |||
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) | |||
end | |||
end | |||
end |
@@ -8,6 +8,7 @@ class REST::StatusSerializer < ActiveModel::Serializer | |||
attribute :favourited, if: :current_user? | |||
attribute :reblogged, if: :current_user? | |||
attribute :muted, if: :current_user? | |||
attribute :pinned, if: :pinnable? | |||
belongs_to :reblog, serializer: REST::StatusSerializer | |||
belongs_to :application | |||
@@ -57,6 +58,21 @@ class REST::StatusSerializer < ActiveModel::Serializer | |||
end | |||
end | |||
def pinned | |||
if instance_options && instance_options[:relationships] | |||
instance_options[:relationships].pins_map[object.id] || false | |||
else | |||
current_user.account.pinned?(object) | |||
end | |||
end | |||
def pinnable? | |||
current_user? && | |||
current_user.account_id == object.account_id && | |||
!object.reblog? && | |||
%w(public unlisted).include?(object.visibility) | |||
end | |||
class ApplicationSerializer < ActiveModel::Serializer | |||
attributes :name, :website | |||
end | |||
@@ -0,0 +1,9 @@ | |||
# frozen_string_literal: true | |||
class StatusPinValidator < ActiveModel::Validator | |||
def validate(pin) | |||
pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog? | |||
pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id | |||
pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility) | |||
end | |||
end |
@@ -30,6 +30,9 @@ | |||
= render 'nothing_here' | |||
- else | |||
.activity-stream.with-header | |||
- if params[:page].to_i.zero? | |||
= render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } | |||
= render partial: 'stream_entries/status', collection: @statuses, as: :status | |||
- if @statuses.size == 20 | |||
@@ -1,4 +1,5 @@ | |||
:ruby | |||
pinned ||= false | |||
include_threads ||= false | |||
is_predecessor ||= false | |||
is_successor ||= false | |||
@@ -25,6 +26,12 @@ | |||
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do | |||
%strong.emojify= display_name(status.account) | |||
= t('stream_entries.reblogged') | |||
- elsif pinned | |||
.pre-header | |||
.pre-header__icon | |||
= fa_icon('thumb-tack fw') | |||
%span | |||
= t('stream_entries.pinned') | |||
= render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper | |||
@@ -434,6 +434,10 @@ en: | |||
statuses: | |||
open_in_web: Open in web | |||
over_character_limit: character limit of %{max} exceeded | |||
pin_errors: | |||
ownership: Someone else's toot cannot be pinned | |||
private: Non-public toot cannot be pinned | |||
reblog: A boost cannot be pinned | |||
show_more: Show more | |||
visibilities: | |||
private: Followers-only | |||
@@ -444,6 +448,7 @@ en: | |||
unlisted_long: Everyone can see, but not listed on public timelines | |||
stream_entries: | |||
click_to_show: Click to show | |||
pinned: Pinned toot | |||
reblogged: boosted | |||
sensitive_content: Sensitive content | |||
terms: | |||
@@ -162,6 +162,9 @@ Rails.application.routes.draw do | |||
resource :mute, only: :create | |||
post :unmute, to: 'mutes#destroy' | |||
resource :pin, only: :create | |||
post :unpin, to: 'pins#destroy' | |||
end | |||
member do | |||
@@ -175,7 +178,8 @@ Rails.application.routes.draw do | |||
resource :public, only: :show, controller: :public | |||
resources :tag, only: :show | |||
end | |||
resources :streaming, only: [:index] | |||
resources :streaming, only: [:index] | |||
get '/search', to: 'search#index', as: :search | |||
@@ -210,6 +214,7 @@ Rails.application.routes.draw do | |||
resource :search, only: :show, controller: :search | |||
resources :relationships, only: :index | |||
end | |||
resources :accounts, only: [:show] do | |||
resources :statuses, only: :index, controller: 'accounts/statuses' | |||
resources :followers, only: :index, controller: 'accounts/follower_accounts' | |||
@@ -245,7 +250,7 @@ Rails.application.routes.draw do | |||
root 'home#index' | |||
match '*unmatched_route', | |||
via: :all, | |||
to: 'application#raise_not_found', | |||
format: false | |||
via: :all, | |||
to: 'application#raise_not_found', | |||
format: false | |||
end |
@@ -0,0 +1,10 @@ | |||
class CreateStatusPins < ActiveRecord::Migration[5.1] | |||
def change | |||
create_table :status_pins do |t| | |||
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false | |||
t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false | |||
end | |||
add_index :status_pins, [:account_id, :status_id], unique: true | |||
end | |||
end |
@@ -10,7 +10,7 @@ | |||
# | |||
# It's strongly recommended that you check this file into your version control system. | |||
ActiveRecord::Schema.define(version: 20170720000000) do | |||
ActiveRecord::Schema.define(version: 20170823162448) do | |||
# These are extensions that must be enabled in order to support this database | |||
enable_extension "plpgsql" | |||
@@ -282,6 +282,14 @@ ActiveRecord::Schema.define(version: 20170720000000) do | |||
t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true | |||
end | |||
create_table "status_pins", force: :cascade do |t| | |||
t.bigint "account_id", null: false | |||
t.bigint "status_id", null: false | |||
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true | |||
t.index ["account_id"], name: "index_status_pins_on_account_id" | |||
t.index ["status_id"], name: "index_status_pins_on_status_id" | |||
end | |||
create_table "statuses", force: :cascade do |t| | |||
t.string "uri" | |||
t.integer "account_id", null: false | |||
@@ -430,6 +438,8 @@ ActiveRecord::Schema.define(version: 20170720000000) do | |||
add_foreign_key "reports", "accounts", on_delete: :cascade | |||
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade | |||
add_foreign_key "session_activations", "users", on_delete: :cascade | |||
add_foreign_key "status_pins", "accounts", on_delete: :cascade | |||
add_foreign_key "status_pins", "statuses", on_delete: :cascade | |||
add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify | |||
add_foreign_key "statuses", "accounts", on_delete: :cascade | |||
add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify | |||
@@ -18,21 +18,37 @@ describe Api::V1::Accounts::StatusesController do | |||
expect(response).to have_http_status(:success) | |||
expect(response.headers['Link'].links.size).to eq(2) | |||
end | |||
end | |||
describe 'GET #index with only media' do | |||
it 'returns http success' do | |||
get :index, params: { account_id: user.account.id, only_media: true } | |||
context 'with only media' do | |||
it 'returns http success' do | |||
get :index, params: { account_id: user.account.id, only_media: true } | |||
expect(response).to have_http_status(:success) | |||
expect(response).to have_http_status(:success) | |||
end | |||
end | |||
end | |||
describe 'GET #index with exclude replies' do | |||
it 'returns http success' do | |||
get :index, params: { account_id: user.account.id, exclude_replies: true } | |||
context 'with exclude replies' do | |||
before do | |||
Fabricate(:status, account: user.account, thread: Fabricate(:status)) | |||
end | |||
expect(response).to have_http_status(:success) | |||
it 'returns http success' do | |||
get :index, params: { account_id: user.account.id, exclude_replies: true } | |||
expect(response).to have_http_status(:success) | |||
end | |||
end | |||
context 'with only pinned' do | |||
before do | |||
Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account)) | |||
end | |||
it 'returns http success' do | |||
get :index, params: { account_id: user.account.id, pinned: true } | |||
expect(response).to have_http_status(:success) | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,57 @@ | |||
# frozen_string_literal: true | |||
require 'rails_helper' | |||
describe Api::V1::Statuses::PinsController do | |||
render_views | |||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | |||
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } | |||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } | |||
context 'with an oauth token' do | |||
before do | |||
allow(controller).to receive(:doorkeeper_token) { token } | |||
end | |||
describe 'POST #create' do | |||
let(:status) { Fabricate(:status, account: user.account) } | |||
before do | |||
post :create, params: { status_id: status.id } | |||
end | |||
it 'returns http success' do | |||
expect(response).to have_http_status(:success) | |||
end | |||
it 'updates the pinned attribute' do | |||
expect(user.account.pinned?(status)).to be true | |||
end | |||
it 'return json with updated attributes' do | |||
hash_body = body_as_json | |||
expect(hash_body[:id]).to eq status.id | |||
expect(hash_body[:pinned]).to be true | |||
end | |||
end | |||
describe 'POST #destroy' do | |||
let(:status) { Fabricate(:status, account: user.account) } | |||
before do | |||
Fabricate(:status_pin, status: status, account: user.account) | |||
post :destroy, params: { status_id: status.id } | |||
end | |||
it 'returns http success' do | |||
expect(response).to have_http_status(:success) | |||
end | |||
it 'updates the pinned attribute' do | |||
expect(user.account.pinned?(status)).to be false | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,4 @@ | |||
Fabricator(:status_pin) do | |||
account | |||
status | |||
end |
@@ -0,0 +1,41 @@ | |||
require 'rails_helper' | |||
RSpec.describe StatusPin, type: :model do | |||
describe 'validations' do | |||
it 'allows pins of own statuses' do | |||
account = Fabricate(:account) | |||
status = Fabricate(:status, account: account) | |||
expect(StatusPin.new(account: account, status: status).save).to be true | |||
end | |||
it 'does not allow pins of statuses by someone else' do | |||
account = Fabricate(:account) | |||
status = Fabricate(:status) | |||
expect(StatusPin.new(account: account, status: status).save).to be false | |||
end | |||
it 'does not allow pins of reblogs' do | |||
account = Fabricate(:account) | |||
status = Fabricate(:status, account: account) | |||
reblog = Fabricate(:status, reblog: status) | |||
expect(StatusPin.new(account: account, status: reblog).save).to be false | |||
end | |||
it 'does not allow pins of private statuses' do | |||
account = Fabricate(:account) | |||
status = Fabricate(:status, account: account, visibility: :private) | |||
expect(StatusPin.new(account: account, status: status).save).to be false | |||
end | |||
it 'does not allow pins of direct statuses' do | |||
account = Fabricate(:account) | |||
status = Fabricate(:status, account: account, visibility: :direct) | |||
expect(StatusPin.new(account: account, status: status).save).to be false | |||
end | |||
end | |||
end |