parent
c5157ef07b
commit
9caa90025f
59 changed files with 493 additions and 29 deletions
|
@ -7,11 +7,14 @@ class AccountsController < ApplicationController
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@pinned_statuses = []
|
||||
|
||||
if current_account && @account.blocking?(current_account)
|
||||
@statuses = []
|
||||
return
|
||||
end
|
||||
|
||||
@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?
|
||||
|
@ -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
|
||||
|
|
28
app/controllers/api/v1/statuses/pins_controller.rb
Normal file
28
app/controllers/api/v1/statuses/pins_controller.rb
Normal file
|
@ -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 = []
|
||||
|
||||
|
|
16
app/models/status_pin.rb
Normal file
16
app/models/status_pin.rb
Normal file
|
@ -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
|
||||
|
|
9
app/validators/status_pin_validator.rb
Normal file
9
app/validators/status_pin_validator.rb
Normal file
|
@ -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,6 +178,7 @@ Rails.application.routes.draw do
|
|||
resource :public, only: :show, controller: :public
|
||||
resources :tag, only: :show
|
||||
end
|
||||
|
||||
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'
|
||||
|
|
10
db/migrate/20170823162448_create_status_pins.rb
Normal file
10
db/migrate/20170823162448_create_status_pins.rb
Normal file
|
@ -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
|
12
db/schema.rb
12
db/schema.rb
|
@ -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,9 +18,8 @@ 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
|
||||
context 'with only media' do
|
||||
it 'returns http success' do
|
||||
get :index, params: { account_id: user.account.id, only_media: true }
|
||||
|
||||
|
@ -28,11 +27,28 @@ describe Api::V1::Accounts::StatusesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET #index with exclude replies' do
|
||||
context 'with exclude replies' do
|
||||
before do
|
||||
Fabricate(:status, account: user.account, thread: Fabricate(:status))
|
||||
end
|
||||
|
||||
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
|
||||
|
|
57
spec/controllers/api/v1/statuses/pins_controller_spec.rb
Normal file
57
spec/controllers/api/v1/statuses/pins_controller_spec.rb
Normal file
|
@ -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
|
4
spec/fabricators/status_pin_fabricator.rb
Normal file
4
spec/fabricators/status_pin_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:status_pin) do
|
||||
account
|
||||
status
|
||||
end
|
41
spec/models/status_pin_spec.rb
Normal file
41
spec/models/status_pin_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue