Merge pull request #5390 from Roardom/pinned-posts

(Add) Pinned posts
This commit is contained in:
Roardom
2026-04-26 10:29:01 +00:00
committed by GitHub
17 changed files with 149 additions and 16 deletions
+9 -7
View File
@@ -37,6 +37,7 @@ use App\Repositories\ChatRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Exception;
use Illuminate\Validation\Rule;
/**
* @see \Tests\Todo\Feature\Http\Controllers\PostControllerTest
@@ -204,10 +205,6 @@ class PostController extends Controller
*/
public function update(Request $request, int $id): \Illuminate\Http\RedirectResponse
{
$request->validate([
'content' => 'required|min:1',
]);
$user = $request->user();
$post = Post::query()->findOrFail($id);
@@ -217,9 +214,14 @@ class PostController extends Controller
abort_unless($post->topic()->authorized(canReplyTopic: true)->exists(), 403);
$post->update([
'content' => $request->input('content'),
]);
$post->update($request->validate([
'content' => 'required|min:1',
'pinned' => [
'sometimes',
'boolean',
Rule::excludeIf(!$user->group->is_modo),
]
]));
return redirect()->to($postUrl)
->with('success', trans('forum.edit-post-success'));
+19 -2
View File
@@ -77,11 +77,28 @@ class TopicPostSearch extends Component
}
}
/**
* @var \Illuminate\Database\Eloquent\Collection<int, Post>
*/
final protected \Illuminate\Database\Eloquent\Collection $pinnedPosts {
get => Post::query()
->with('user.group')
->withCount('likes', 'dislikes', 'authorPosts', 'authorTopics')
->withSum('tips', 'bon')
->where('topic_id', '=', $this->topic->id)
->where('pinned', '=', true)
->authorized(canReadTopic: true)
->when($this->search !== '', fn ($query) => $query->where('content', 'LIKE', '%'.$this->search.'%'))
->orderBy('created_at')
->get();
}
final public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Contracts\Foundation\Application
{
return view('livewire.topic-post-search', [
'topic' => $this->topic,
'posts' => $this->posts,
'topic' => $this->topic,
'posts' => $this->posts,
'pinnedPosts' => $this->pinnedPosts,
]);
}
}
+4 -1
View File
@@ -29,6 +29,7 @@ use AllowDynamicProperties;
* @property int $id
* @property string $content
* @property bool $anon
* @property bool $pinned
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property int $user_id
@@ -50,6 +51,7 @@ final class Post extends Model
protected $fillable = [
'content',
'anon',
'pinned',
'topic_id',
'user_id',
];
@@ -62,7 +64,8 @@ final class Post extends Model
protected function casts(): array
{
return [
'anon' => 'bool',
'anon' => 'bool',
'pinned' => 'bool',
];
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
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('posts', function (Blueprint $table): void {
$table->boolean('pinned')->default(false)->after('anon');
$table->index(['pinned', 'topic_id']);
});
}
};
+3
View File
@@ -1357,6 +1357,7 @@ CREATE TABLE `posts` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`content` text COLLATE utf8mb4_unicode_ci NOT NULL,
`anon` tinyint(1) NOT NULL DEFAULT '0',
`pinned` tinyint(1) NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`user_id` int unsigned NOT NULL,
@@ -1365,6 +1366,7 @@ CREATE TABLE `posts` (
KEY `fk_posts_topics1_idx` (`topic_id`),
KEY `posts_created_at_index` (`created_at`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_pinned_topic_id_index` (`pinned`,`topic_id`),
CONSTRAINT `posts_topic_id_foreign` FOREIGN KEY (`topic_id`) REFERENCES `topics` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -3128,3 +3130,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (376,'2026_02_03_01
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (377,'2026_02_04_184040_combine_user_audibles_echoes',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (378,'2026_02_18_023757_change_donation_dates_to_timestamps',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (379,'2026_03_10_033755_migrate_request_bounty_created_at_to_updated_at',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (380,'2026_04_22_061705_add_pinned_column_to_posts',1);
+29 -6
View File
@@ -21,6 +21,13 @@
font-size: 22px;
}
.topic-posts__pinned-separator {
width: 100%;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--post-pinned-separator-fg);
}
.post {
display: grid;
grid-template-areas:
@@ -38,16 +45,26 @@
.post__header {
grid-area: header;
display: grid;
grid-template-areas: 'datetime topic . tip-stats toolbar';
grid-template-columns: auto auto 1fr auto auto;
grid-template-areas: 'pin datetime topic . tip-stats toolbar';
grid-template-columns: auto auto auto 1fr auto auto;
align-items: center;
gap: 9px;
font-size: 13px;
color: var(--post-head-fg);
background-color: var(--post-head-bg);
padding-left: 9px;
}
.post__pin,
.post__datetime,
.post__topic,
.post__tip-stats {
margin-right: 9px;
}
.post__pin {
grid-area: pin;
}
.post__datetime {
grid-area: datetime;
}
@@ -289,9 +306,15 @@
}
.post__header {
grid-template-areas: 'datetime topic . tip-stats overflow' '. . . . toolbar';
grid-template-columns: auto auto 1fr auto auto;
gap: 0 6px;
grid-template-areas: 'pin datetime topic . tip-stats overflow' '. . . . . toolbar';
grid-template-columns: auto auto auto 1fr auto auto;
}
.post__pin,
.post__datetime,
.post__topic,
.post__tip-stats {
margin-right: 6px;
}
.post__aside {
+1
View File
@@ -217,6 +217,7 @@
--post-shadow: none;
--post-head-fg: var(--text-color);
--post-head-bg: none;
--post-pinned-separator-fg: #404040;
--post-toolbar-bg: #242424;
--post-toolbar-fg: #b0b0b0;
--post-toolbar-hover-bg: #2a2a2a;
+1
View File
@@ -268,6 +268,7 @@
--post-shadow: none;
--post-head-fg: var(--text-color);
--post-head-bg: none;
--post-pinned-separator-fg: #404040;
--post-toolbar-bg: #1d1d1d;
--post-toolbar-fg: #ccc;
--post-toolbar-hover-bg: #262626;
+1
View File
@@ -229,6 +229,7 @@
0 1px 8px 0 rgba(0, 0, 0, 0.2);
--post-head-fg: var(--panel-fg);
--post-head-bg: none;
--post-pinned-separator-fg: #aaa;
--post-toolbar-bg: #989898;
--post-toolbar-fg: #e5e5e5;
--post-toolbar-hover-bg: #777;
@@ -229,6 +229,7 @@
0 1px 8px 0 rgba(0, 0, 0, 0.2);
--post-head-fg: var(--panel-fg);
--post-head-bg: none;
--post-pinned-separator-fg: #222;
--post-toolbar-bg: #000;
--post-toolbar-fg: #999;
--post-toolbar-hover-bg: #000;
@@ -234,6 +234,7 @@
0 1px 8px 0 rgba(0, 0, 0, 0.2);
--post-head-fg: var(--panel-fg);
--post-head-bg: none;
--post-pinned-separator-fg: #444;
--post-toolbar-bg: #343338;
--post-toolbar-fg: #999;
--post-toolbar-hover-bg: #343338;
@@ -233,6 +233,7 @@
0 1px 8px 0 rgba(0, 0, 0, 0.2);
--post-head-fg: var(--panel-fg);
--post-head-bg: none;
--post-pinned-separator-fg: #bbb;
--post-toolbar-bg: #fff;
--post-toolbar-fg: #777;
--post-toolbar-hover-bg: #fff;
@@ -235,6 +235,7 @@
0 1px 8px 0 rgba(0, 0, 0, 0.2);
--post-head-fg: var(--panel-fg);
--post-head-bg: none;
--post-pinned-separator-fg: #222;
--post-toolbar-bg: #0f111a;
--post-toolbar-fg: #999;
--post-toolbar-hover-bg: #0f111a;
+1
View File
@@ -247,6 +247,7 @@
--post-shadow: none;
--post-head-fg: var(--text-color);
--post-head-bg: #232323;
--post-pinned-separator-fg: #404040;
--post-toolbar-bg: #232323;
--post-toolbar-fg: #ccc;
--post-toolbar-hover-bg: #262626;
@@ -11,6 +11,13 @@
)"
>
<header class="post__header">
@if ($post->pinned)
<i
class="{{ config('other.font-awesome') }} fa-thumbtack post__pin"
title="Pinned"
></i>
@endif
<time
class="post__datetime"
datetime="{{ $post->created_at }}"
+18
View File
@@ -61,6 +61,24 @@
@csrf
@method('PATCH')
@livewire('bbcode-input', ['name' => 'content', 'label' => __('forum.post'), 'content' => $post->content])
@if (auth()->user()->group->is_modo)
<p class="form__group">
<input type="hidden" name="pinned" value="0" />
<input
type="checkbox"
class="form__checkbox"
id="pinned"
name="pinned"
value="1"
@checked(old('pinned') ?? $post->pinned)
/>
<label class="form__label" for="pinned">
{{ __('forum.pin') }}
</label>
</p>
@endif
<button class="form__button form__button--filled">
{{ __('common.submit') }}
</button>
@@ -58,11 +58,31 @@
<div class="panel__body">
@if ($posts->count() > 0)
<ol class="topic-posts">
@foreach ($pinnedPosts as $post)
<li class="topic-posts__item">
<x-forum.post :post="$post" />
</li>
@endforeach
@if ($pinnedPosts->isNotEmpty())
<hr class="topic-posts__pinned-separator" />
@endif
@foreach ($posts as $post)
<li class="topic-posts__item">
<x-forum.post :post="$post" />
</li>
@endforeach
@if ($pinnedPosts->isNotEmpty())
<hr class="topic-posts__pinned-separator" />
@endif
@foreach ($pinnedPosts as $post)
<li class="topic-posts__item">
<x-forum.post :post="$post" />
</li>
@endforeach
</ol>
@else
No topics.