Felipe Mateus 3 ماه پیش
والد
کامیت
cce4c41d97

+ 49 - 0
app/Http/Controllers/Api/ApiV1Controller.php

@@ -4426,4 +4426,53 @@ class ApiV1Controller extends Controller
             })
         );
     }
+
+    /**
+     *  GET /api/v2/statuses/{id}/pin
+     */
+    public function statusPin(Request $request, $id) {
+        abort_if(! $request->user(), 403);
+        $status = Status::findOrFail($id);
+        $user = $request->user();
+
+        $res = [
+            'status' => false,
+            'message' => ''
+        ];
+
+        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);
+        }
+
+
+        return $this->json("")->setStatusCode(400);
+    }
+
+
+    /**
+     *  GET /api/v2/statuses/{id}/unpin
+     */
+    public function statusUnpin(Request $request, $id) {
+
+        abort_if(! $request->user(), 403);
+        $status = Status::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);
+        }
+
+        return $this->json("")->setStatusCode(200);
+    }
+
 }

+ 1 - 0
app/Http/Controllers/PublicApiController.php

@@ -725,6 +725,7 @@ class PublicApiController extends Controller
             ->where('id', $dir, $id)
             ->whereIn('scope', $visibility)
             ->limit($limit)
+            ->orderBy('pinned_order')
             ->orderByDesc('id')
             ->get()
             ->map(function ($s) use ($user) {

+ 44 - 0
app/Services/StatusService.php

@@ -11,6 +11,8 @@ use League\Fractal\Serializer\ArraySerializer;
 class StatusService
 {
     const CACHE_KEY = 'pf:services:status:v1.1:';
+    const MAX_PINNED = 3;
+
 
     public static function key($id, $publicOnly = true)
     {
@@ -198,4 +200,46 @@ class StatusService
     {
         return InstanceService::totalLocalStatuses();
     }
+
+    public static function isPinned($id)
+    {
+        $status = Status::find($id);
+        return $status && $status->whereNotNull("pinned_order")->count() > 0;
+    }
+
+    public static  function totalPins($pid)
+    {
+        return Status::whereProfileId($pid)->whereNotNull("pinned_order")->count();
+    }
+
+    public static function markPin($id)
+    {
+        $status = Status::find($id);
+
+        if (self::isPinned($id)) {
+            return true;
+        }
+        $totalPins = self::totalPins($status->profile_id);
+
+        if ($totalPins >= self::MAX_PINNED) {
+            return false;
+        }
+
+        $status->pinned_order = $totalPins + 1;
+        $status->save();
+
+        self::refresh($id);
+        return true;
+    }
+
+    public static function unmarkPin($id)
+    {
+        $status = Status::find($id);
+
+        $status->pinned_order = null;
+        $status->save();
+
+        self::refresh($id);
+        return true;
+    }
 }

+ 1 - 0
app/Transformer/Api/StatusStatelessTransformer.php

@@ -69,6 +69,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
             'tags' => StatusHashtagService::statusTags($status->id),
             'poll' => $poll,
             'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
+            'pinned' => (bool) $status->pinned_order,
         ];
     }
 }

+ 1 - 0
app/Transformer/Api/StatusTransformer.php

@@ -71,6 +71,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
             'poll' => $poll,
             'bookmarked' => BookmarkService::get($pid, $status->id),
             'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
+            'pinned' => (bool) $status->pinned_order,
         ];
     }
 }

+ 30 - 0
database/migrations/2025_03_19_022553_add_pinned_columns_statuses_table.php

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('statuses', function (Blueprint $table) {
+            $table->integer('pinned_order')->nullable()->default(null);
+
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        //
+        Schema::table('statuses', function (Blueprint $table) {
+            $table->dropColumn('pinned_order');
+        });
+    }
+};

+ 49 - 0
resources/assets/components/partials/post/ContextMenu.vue

@@ -112,6 +112,21 @@
                     @click.prevent="unarchivePost(status)">
                     {{ $t('menu.unarchive') }}
                 </a>
+                <a
+                    v-if="status && profile.id == status.account.id && !status.pinned"
+                    class="list-group-item menu-option text-danger"
+                    href="#"
+                    @click.prevent="pinPost(status)">
+                    {{ $t('menu.pin') }}
+                </a>
+
+                <a
+                    v-if="status && profile.id == status.account.id && status.pinned"
+                    class="list-group-item menu-option text-danger"
+                    href="#"
+                    @click.prevent="unpinPost(status)">
+                    {{ $t('menu.unpin') }}
+                </a>
 
                 <a
                     v-if="config.ab.pue && status && profile.id == status.account.id && status.visibility !== 'archived'"
@@ -976,6 +991,40 @@
                     }
                 })
             },
+
+            pinPost(status) {
+                if(window.confirm(this.$t('menu.pinPostConfirm')) == false) {
+                    return;
+                }
+
+                axios.post('/api/v2/statuses/' + status.id + '/pin')
+                .then(res => {
+                    const data = res.data;
+                    if(data.status){
+                        swal('Success', "Post was pinned successfully!" , 'success');
+                    }else {
+                        swal('Error', data.message, 'error');
+                    }
+                    this.closeModals();
+                });
+            },
+
+            unpinPost(status) {
+                if(window.confirm(this.$t('menu.unpinPostConfirm')) == false) {
+                    return;
+                }
+
+                axios.post('/api/v2/statuses/' + status.id + '/unpin')
+                .then(res => {
+                    const data = res.data;
+                    if(data.status){
+                        swal('Success', "Post was unpinned successfully!" , 'success');
+                    }else {
+                        swal('Error', data.message, 'error');
+                    }
+                    this.closeModals();
+                });
+            },
         }
     }
 </script>

+ 19 - 0
resources/assets/components/partials/profile/ProfileFeed.vue

@@ -181,6 +181,9 @@
 						<span class="badge badge-light timestamp-overlay-badge">
 							{{ timeago(s.created_at) }}
 						</span>
+                        <span v-if="s.pinned" class="badge badge-light pinned-overlay-badge">
+						    <i class="fa fa-tag" aria-hidden="true"></i>
+						</span>
 					</a>
 				</div>
 
@@ -219,6 +222,10 @@
 							<span class="badge badge-light timestamp-overlay-badge">
 								{{ timeago(s.created_at) }}
 							</span>
+
+                            <span v-if="s.pinned" class="badge badge-light pinned-overlay-badge">
+						        <i class="fa fa-tag" aria-hidden="true"></i>
+						    </span>
 						</a>
 
 						<a v-else-if="s.sensitive" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
@@ -246,6 +253,9 @@
 							<span class="badge badge-light timestamp-overlay-badge">
 								{{ timeago(s.created_at) }}
 							</span>
+                            <span v-if="s.pinned" class="badge badge-light pinned-overlay-badge">
+						        <i class="fa fa-tag" aria-hidden="true"></i>
+						    </span>
 						</a>
 					</div>
 
@@ -1071,6 +1081,7 @@
 					});
 				});
 			},
+
 		}
 	}
 </script>
@@ -1126,6 +1137,14 @@
 			opacity: 0.6;
 		}
 
+        .pinned-overlay-badge {
+			position: absolute;
+			top: 10px;
+			left: 10px;
+            color: var(--dark);
+            font-size: 120%;
+		}
+
 		.profile-nav-btns {
 			margin-right: 1rem;
 

+ 8 - 4
resources/lang/en/web.php

@@ -129,10 +129,10 @@ return [
 		'emptyPosts' => 'We can\'t seem to find any posts',
 	],
 
-	'menu' => [
-		'viewPost' => 'View Post',
-		'viewProfile' => 'View Profile',
-		'moderationTools' => 'Moderation Tools',
+    'menu' => [
+        'viewPost' => 'View Post',
+        'viewProfile' => 'View Profile',
+        'moderationTools' => 'Moderation Tools',
 		'report' => 'Report',
 		'archive' => 'Archive',
 		'unarchive' => 'Unarchive',
@@ -176,6 +176,10 @@ return [
 		'deletePostConfirm' => 'Are you sure you want to delete this post?',
 		'archivePostConfirm' => 'Are you sure you want to archive this post?',
 		'unarchivePostConfirm' => 'Are you sure you want to unarchive this post?',
+        'pin' => "Pin",
+        'unpin' => "Unpin",
+        'pinPostConfirm' => 'Are you sure you want to pin this post?',
+        'unpinPostConfirm' => 'Are you sure you want to unpin this post?'
 	],
 
 	'story' => [

+ 4 - 0
resources/lang/pt/web.php

@@ -176,6 +176,10 @@ return [
 		'deletePostConfirm' => 'Tem a certeza que pretende apagar esta publicação?',
 		'archivePostConfirm' => 'Tem a certeza que pretende arquivar esta publicação?',
 		'unarchivePostConfirm' => 'Tem a certeza que pretende desarquivar este post?',
+        'pin' => "Fixar",
+        'unpin' => "Desfixar",
+        "pinPostConfirm" => "Tem certeza de que deseja fixar esta publicação?",
+        "unpinPostConfirm" => "Tem certeza de que deseja desafixar esta publicação?"
 	],
 
 	'story' => [

+ 2 - 0
routes/web-api.php

@@ -58,6 +58,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::get('discover/tag', 'DiscoverController@getHashtags');
             Route::get('statuses/{id}/replies', 'Api\ApiV1Controller@statusReplies');
             Route::get('statuses/{id}/state', 'Api\ApiV1Controller@statusState');
+            Route::post('statuses/{id}/pin', 'Api\ApiV1Controller@statusPin');
+            Route::post('statuses/{id}/unpin', 'Api\ApiV1Controller@statusUnpin');
         });
 
         Route::group(['prefix' => 'pixelfed'], function() {