commit: 9caa90025fd9f1ef46a74f31cefd19335e291e76
parent: c5157ef07bbae5c3a307d6a005aef0f1c0452af3
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 25 Aug 2017 01:41:18 +0200
Pinned statuses (#4675)
* Pinned statuses
* yarn manage:translations
Diffstat:
59 files changed, 493 insertions(+), 29 deletions(-)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -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
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
@@ -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,
+ };
+};
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
@@ -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,
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
@@ -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 });
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
@@ -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')));
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -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 });
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
@@ -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}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
@@ -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": "الرئيسية",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
@@ -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": "Начало",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
@@ -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"
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.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",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
@@ -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": "خانه",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
@@ -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": "בבית",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
@@ -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": "ホーム",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
@@ -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": "홈",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
@@ -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": "Главная",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.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",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
@@ -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": "Головна",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
@@ -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": "主页",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
@@ -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": "主頁",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
@@ -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": "家",
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
@@ -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);
diff --git a/app/models/account.rb b/app/models/account.rb
@@ -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
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
@@ -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
diff --git a/app/models/status.rb b/app/models/status.rb
@@ -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 = []
diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb
@@ -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
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
@@ -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
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
@@ -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
diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb
@@ -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
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
@@ -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
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
@@ -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:
diff --git a/config/routes.rb b/config/routes.rb
@@ -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
diff --git a/db/migrate/20170823162448_create_status_pins.rb b/db/migrate/20170823162448_create_status_pins.rb
@@ -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
diff --git a/db/schema.rb b/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
diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -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
diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:status_pin) do
+ account
+ status
+end
diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb
@@ -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