Fix pinned posts implementation

pull/5914/head
Daniel Supernault 3 days ago
parent f70e0b4ae0
commit 2f655d0008
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -763,7 +763,8 @@ class ApiV1Controller extends Controller
'reblog_of_id',
'type',
'id',
'scope'
'scope',
'pinned_order'
)
->whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
@ -4439,49 +4440,56 @@ class ApiV1Controller extends Controller
/**
* GET /api/v2/statuses/{id}/pin
*/
public function statusPin(Request $request, $id) {
public function statusPin(Request $request, $id)
{
abort_if(! $request->user(), 403);
$status = Status::findOrFail($id);
$user = $request->user();
$status = Status::whereScope('public')->find($id);
$res = [
'status' => false,
'message' => ''
];
if (! $status) {
return $this->json(['error' => 'Record not found'], 404);
}
if($status->profile_id == $user->profile_id){
if(StatusService::markPin($status->id)){
$res['status'] = true;
} else {
$res['message'] = 'Limit pin reached';
}
return $this->json($res)->setStatusCode(200);
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422);
}
$res = StatusService::markPin($status->id);
return $this->json("")->setStatusCode(400);
}
if (! $res['success']) {
return $this->json([
'error' => $res['error'],
], 422);
}
$statusRes = StatusService::get($status->id, true, true);
$status['pinned'] = true;
return $this->json($statusRes);
}
/**
* GET /api/v2/statuses/{id}/unpin
*/
public function statusUnpin(Request $request, $id) {
public function statusUnpin(Request $request, $id)
{
abort_if(! $request->user(), 403);
$status = Status::findOrFail($id);
$status = Status::whereScope('public')->findOrFail($id);
$user = $request->user();
if($status->profile_id == $user->profile_id){
StatusService::unmarkPin($status->id);
$res = [
'status' => true,
'message' => ''
];
return $this->json($res)->setStatusCode(200);
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => 'Record not found'], 404);
}
return $this->json("")->setStatusCode(200);
}
$res = StatusService::unmarkPin($status->id);
if (! $res) {
return $this->json($res, 422);
}
$status = StatusService::get($status->id, true, true);
$status['pinned'] = false;
return $this->json($status);
}
}

@ -667,10 +667,8 @@ class PublicApiController extends Controller
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:24',
'cursor' => 'nullable',
]);
$user = $request->user();
@ -683,84 +681,137 @@ class PublicApiController extends Controller
}
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;
$scope = ['photo', 'photo:album', 'video', 'video:album'];
$onlyMedia = $request->input('only_media', true);
$pinned = $request->filled('pinned') && $request->boolean('pinned') == true;
$hasCursor = $request->filled('cursor');
$visibility = $this->determineVisibility($profile, $user);
if (empty($visibility)) {
return response()->json([]);
}
$result = collect();
$remainingLimit = $limit;
if (! $min_id && ! $max_id) {
$min_id = 1;
if ($pinned && ! $hasCursor) {
$pinnedStatuses = Status::whereProfileId($profile['id'])
->whereNotNull('pinned_order')
->orderBy('pinned_order')
->get();
$pinnedResult = $this->processStatuses($pinnedStatuses, $user, $onlyMedia);
$result = $pinnedResult;
$remainingLimit = max(1, $limit - $pinnedResult->count());
}
$paginator = Status::whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->when($pinned, function ($query) {
return $query->whereNull('pinned_order');
})
->whereIn('type', $scope)
->whereIn('scope', $visibility)
->orderByDesc('id')
->cursorPaginate($remainingLimit)
->withQueryString();
$headers = $this->generatePaginationHeaders($paginator);
$regularStatuses = $this->processStatuses($paginator->items(), $user, $onlyMedia);
$result = $result->concat($regularStatuses);
return response()->json($result, 200, $headers);
}
private function determineVisibility($profile, $user)
{
if ($profile['id'] == $user->profile_id) {
return ['public', 'unlisted', 'private'];
}
if ($profile['locked']) {
if (! $user) {
return response()->json([]);
return [];
}
$pid = $user->profile_id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
$isFollowing = Follower::whereProfileId($pid)
->whereFollowingId($profile['id'])
->exists();
return $following->push($pid)->toArray();
});
$visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : [];
return $isFollowing ? ['public', 'unlisted', 'private'] : ['public'];
} else {
if ($user) {
$pid = $user->profile_id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
$isFollowing = Follower::whereProfileId($pid)
->whereFollowingId($profile['id'])
->exists();
return $following->push($pid)->toArray();
});
$visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
return $isFollowing ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else {
$visibility = ['public', 'unlisted'];
return ['public', 'unlisted'];
}
}
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$res = Status::whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('scope', $visibility)
->limit($limit)
->orderBy('pinned_order')
->orderByDesc('id')
->get()
->map(function ($s) use ($user) {
try {
$status = StatusService::get($s->id, false);
if (! $status) {
return false;
}
} catch (\Exception $e) {
$status = false;
}
private function processStatuses($statuses, $user, $onlyMedia)
{
return collect($statuses)->map(function ($status) use ($user) {
try {
$mastodonStatus = StatusService::getMastodon($status->id, false);
if (! $mastodonStatus) {
return null;
}
if ($user && $status) {
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
if ($user) {
$mastodonStatus['favourited'] = (bool) LikeService::liked($user->profile_id, $status->id);
}
return $status;
})
->filter(function ($s) use ($onlyMedia) {
if (! $s) {
return $mastodonStatus;
} catch (\Exception $e) {
return null;
}
})
->filter(function ($status) use ($onlyMedia) {
if (! $status) {
return false;
}
if ($onlyMedia) {
if (
! isset($s['media_attachments']) ||
! is_array($s['media_attachments']) ||
empty($s['media_attachments'])
) {
return false;
}
return isset($status['media_attachments']) &&
is_array($status['media_attachments']) &&
! empty($status['media_attachments']);
}
return $s;
return true;
})
->values();
}
return response()->json($res);
/**
* Generate pagination link headers from paginator
*/
private function generatePaginationHeaders($paginator)
{
$link = null;
if ($paginator->onFirstPage()) {
if ($paginator->hasMorePages()) {
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
}
} else {
if ($paginator->previousPageUrl()) {
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
}
if ($paginator->hasMorePages()) {
$link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
}
}
return isset($link) ? ['Link' => $link] : [];
}
}

@ -11,8 +11,8 @@ use League\Fractal\Serializer\ArraySerializer;
class StatusService
{
const CACHE_KEY = 'pf:services:status:v1.1:';
const MAX_PINNED = 3;
const MAX_PINNED = 3;
public static function key($id, $publicOnly = true)
{
@ -84,7 +84,6 @@ class StatusService
$status['shortcode'],
$status['taggedPeople'],
$status['thread'],
$status['pinned'],
$status['account']['header_bg'],
$status['account']['is_admin'],
$status['account']['last_fetched_at'],
@ -203,43 +202,86 @@ class StatusService
public static function isPinned($id)
{
$status = Status::find($id);
return $status && $status->whereNotNull("pinned_order")->count() > 0;
return Status::whereId($id)->whereNotNull('pinned_order')->exists();
}
public static function totalPins($pid)
public static function totalPins($pid)
{
return Status::whereProfileId($pid)->whereNotNull("pinned_order")->count();
return Status::whereProfileId($pid)->whereNotNull('pinned_order')->count();
}
public static function markPin($id)
{
$status = Status::find($id);
if (! $status) {
return [
'success' => false,
'error' => 'Record not found',
];
}
if ($status->scope != 'public') {
return [
'success' => false,
'error' => 'Validation failed: you can only pin public posts',
];
}
if (self::isPinned($id)) {
return true;
return [
'success' => false,
'error' => 'This post is already pinned',
];
}
$totalPins = self::totalPins($status->profile_id);
if ($totalPins >= self::MAX_PINNED) {
return false;
return [
'success' => false,
'error' => 'Validation failed: You have already pinned the max number of posts',
];
}
$status->pinned_order = $totalPins + 1;
$status->save();
self::refresh($id);
return true;
return [
'success' => true,
'error' => null,
];
}
public static function unmarkPin($id)
{
$status = Status::find($id);
if (! $status || is_null($status->pinned_order)) {
return false;
}
$removedOrder = $status->pinned_order;
$profileId = $status->profile_id;
$status->pinned_order = null;
$status->save();
Status::where('profile_id', $profileId)
->whereNotNull('pinned_order')
->where('pinned_order', '>', $removedOrder)
->orderBy('pinned_order', 'asc')
->chunk(10, function ($statuses) {
foreach ($statuses as $s) {
$s->pinned_order = $s->pinned_order - 1;
$s->save();
}
});
self::refresh($id);
return true;
}
}

@ -85,6 +85,8 @@
:profile="user"
@report-modal="handleReport()"
@delete="deletePost()"
@pinned="handlePinned()"
@unpinned="handleUnpinned()"
v-on:edit="handleEdit"
/>
@ -441,7 +443,15 @@
this.$nextTick(() => {
this.forceUpdateIdx++;
});
}
},
handlePinned() {
this.post.pinned = true;
},
handleUnpinned() {
this.post.pinned = false;
},
}
}
</script>

@ -997,15 +997,23 @@
return;
}
axios.post('/api/v2/statuses/' + status.id + '/pin')
this.closeModals();
axios.post('/api/v2/statuses/' + status.id.toString() + '/pin')
.then(res => {
const data = res.data;
if(data.status){
swal('Success', "Post was pinned successfully!" , 'success');
}else {
swal('Error', data.message, 'error');
if(data.id && data.pinned) {
this.$emit('pinned');
swal('Pinned', 'Successfully pinned post to your profile', 'success');
} else {
swal('Error', 'An error occured when attempting to pin', 'error');
}
})
.catch(err => {
this.closeModals();
if(err.response?.data?.error) {
swal('Error', err.response?.data?.error, 'error');
}
});
},
@ -1013,16 +1021,25 @@
if(window.confirm(this.$t('menu.unpinPostConfirm')) == false) {
return;
}
this.closeModals();
axios.post('/api/v2/statuses/' + status.id + '/unpin')
axios.post('/api/v2/statuses/' + status.id.toString() + '/unpin')
.then(res => {
const data = res.data;
if(data.status){
swal('Success', "Post was unpinned successfully!" , 'success');
}else {
swal('Error', data.message, 'error');
if(data.id) {
this.$emit('unpinned');
swal('Unpinned', 'Successfully unpinned post from your profile', 'success');
} else {
swal('Error', data.error, 'error');
}
})
.catch(err => {
this.closeModals();
if(err.response?.data?.error) {
swal('Error', err.response?.data?.error, 'error');
} else {
window.location.reload()
}
});
},
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save