add: store unread news notifications

This commit is contained in:
Roardom
2025-06-17 10:41:04 +00:00
parent 99800ce14e
commit 9e63d82297
9 changed files with 177 additions and 37 deletions
+4 -1
View File
@@ -17,6 +17,7 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
/**
* @see \Tests\Feature\Http\Controllers\ArticleControllerTest
@@ -36,8 +37,10 @@ class ArticleController extends Controller
/**
* Show A Article.
*/
public function show(Article $article): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
public function show(Request $request, Article $article): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
$article->unreads()->whereBelongsTo($request->user())->delete();
return view('article.show', [
'article' => $article->load(['user', 'comments']),
]);
+6 -9
View File
@@ -46,13 +46,6 @@ class HomeController extends Controller
// Authorized User
$user = $request->user();
// Latest Articles/News Block
$articles = cache()->remember('latest_article', $expiresAt, fn () => Article::latest()->take(1)->get());
foreach ($articles as $article) {
$article->newNews = ($user->last_login->subDays(3)->getTimestamp() < $article->created_at->getTimestamp()) ? 1 : 0;
}
return view('home.index', [
'user' => $user,
'users' => cache()->remember(
@@ -82,8 +75,12 @@ class HomeController extends Controller
->oldest('position')
->get()
),
'articles' => $articles,
'topics' => Topic::query()
'articles' => Article::query()
->latest()
->limit(3)
->withExists(['unreads' => fn ($query) => $query->whereBelongsTo($user)])
->get(),
'topics' => Topic::query()
->with(['user', 'user.group', 'latestPoster', 'reads' => fn ($query) => $query->whereBelongsTo($user)])
->authorized(canReadTopic: true)
->latest()
@@ -20,6 +20,8 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Staff\StoreArticleRequest;
use App\Http\Requests\Staff\UpdateArticleRequest;
use App\Models\Article;
use App\Models\UnreadArticle;
use App\Models\User;
use Intervention\Image\Facades\Image;
use Exception;
use Illuminate\Support\Facades\Storage;
@@ -65,7 +67,15 @@ class ArticleController extends Controller
Image::make($image->getRealPath())->fit(75, 75)->encode('png', 100)->save($path);
}
Article::create(['user_id' => $request->user()->id, 'image' => $filename ?? null] + $request->validated());
$article = Article::create(['user_id' => $request->user()->id, 'image' => $filename ?? null] + $request->validated());
UnreadArticle::query()->insertUsing(
['article_id', 'user_id'],
User::query()
->selectRaw('?', [$article->id])
->addSelect('id')
->whereHas('group', fn ($query) => $query->whereNotIn('slug', ['validating', 'pruned', 'banned', 'disabled']))
);
return to_route('staff.articles.index')
->with('success', 'Your article has successfully published!');
+8
View File
@@ -65,4 +65,12 @@ class Article extends Model
{
return $this->morphMany(Comment::class, 'commentable');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany<UnreadArticle, $this>
*/
public function unreads(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->HasMany(UnreadArticle::class);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?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
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* App\Models\UnreadArticle.
*
* @property int $id
* @property int $article_id
* @property int $user_id
*/
class UnreadArticle extends Model
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The attributes that aren't mass assignable.
*
* @var string[]
*/
protected $guarded = [];
/**
* Belongs to an article.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Torrent, $this>
*/
public function article(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Article::class);
}
/**
* Belongs to a user.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<User, $this>
*/
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
}
+1
View File
@@ -62,6 +62,7 @@
"unmark",
"unmoderated",
"unparticipated",
"unreads",
"unsatisfieds",
"unsnooze",
"unsticky",
@@ -0,0 +1,40 @@
<?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('articles', function (Blueprint $table): void {
$table->increments('id')->change();
});
Schema::create('unread_articles', function (Blueprint $table): void {
$table->increments('id');
$table->unsignedInteger('article_id');
$table->unsignedInteger('user_id');
$table->foreign('article_id')->references('id')->on('articles')->cascadeOnUpdate()->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete();
});
}
};
@@ -1,3 +1,9 @@
.article-preview-wrapper.article-preview-wrapper {
display: flex;
flex-direction: column;
row-gap: 2rem;
}
.article-preview {
padding: 18px;
border-radius: 5px;
@@ -32,6 +38,10 @@
font-size: 22px;
}
.article-preview__title svg {
max-height: 15px;
}
.article-preview__link {
color: var(--article-card-head-fg);
}
+34 -26
View File
@@ -1,30 +1,38 @@
@foreach ($articles as $article)
<section class="panelV2 blocks__news" x-data="{ show: {{ $article->newNews }} }">
<header class="panel__header" x-on:click="show = !show" style="cursor: pointer">
<h2 class="panel__heading panel__heading--centered">
@if ($article->newNews)
@joypixels(':rotating_light:')
{{ __('blocks.new-news') }} {{ $article->created_at->diffForHumans() }}
@joypixels(':rotating_light:')
@else
{{ __('blocks.check-news') }} {{ $article->created_at->diffForHumans() }}
@endif
</h2>
<div class="panel__actions">
<div class="panel__action">
<a
href="{{ route('articles.index') }}"
class="form__button form__button--text"
>
{{ __('common.view-all') }}
</a>
</div>
<section
class="panelV2 blocks__news"
x-data="{
show: {{ Js::from($articles->contains(fn ($article) => $article->unread_news_exists)) }},
}"
>
<header class="panel__header" x-on:click="show = !show" style="cursor: pointer">
<h2 class="panel__heading panel__heading--centered">
@if ($articles->first()?->unread_news_exists)
@joypixels(':rotating_light:')
{{ __('blocks.new-news') }}
{{ $articles->first()?->created_at?->diffForHumans() }}
@joypixels(':rotating_light:')
@else
{{ __('blocks.check-news') }}
{{ $articles->first()?->created_at?->diffForHumans() }}
@endif
</h2>
<div class="panel__actions">
<div class="panel__action">
<a href="{{ route('articles.index') }}" class="form__button form__button--text">
{{ __('common.view-all') }}
</a>
</div>
</header>
<div class="panel__body" x-cloak x-show="show">
</div>
</header>
<div class="panel__body article-preview-wrapper" x-cloak x-show="show">
@foreach ($articles as $article)
<article class="article-preview">
<header class="article-preview__header">
<h2 class="article-preview__title">
@if ($article->unread_news_exists)
<x-animation.notification />
@endif
<a
class="article-preview__link"
href="{{ route('articles.show', ['article' => $article]) }}"
@@ -55,6 +63,6 @@
{{ __('articles.read-more') }}
</a>
</article>
</div>
</section>
@endforeach
@endforeach
</div>
</section>