Skip to content

Commit ff3bcdb

Browse files
authored
Merge pull request #30 from laravelcm/notifications
Notifications
2 parents 269b8d6 + 8fa2624 commit ff3bcdb

26 files changed

+507
-8
lines changed

app/Actions/Fortify/CreateNewUser.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\User;
66
use Illuminate\Support\Facades\Hash;
77
use Illuminate\Support\Facades\Validator;
8+
use Illuminate\Support\Str;
89
use Illuminate\Validation\Rule;
910
use Laravel\Fortify\Contracts\CreatesNewUsers;
1011

@@ -35,6 +36,7 @@ public function create(array $input): User
3536
'string',
3637
'min:6',
3738
'max:20',
39+
'alpha_dash',
3840
Rule::unique(User::class, 'username'),
3941
],
4042
'password' => $this->passwordRules(),
@@ -43,9 +45,9 @@ public function create(array $input): User
4345
return User::create([
4446
'name' => $input['name'],
4547
'email' => $input['email'],
46-
'username' => $input['username'],
47-
'opt_in' => isset($input['opt_in']),
48+
'username' => Str::lower($input['username']),
4849
'password' => Hash::make($input['password']),
50+
'opt_in' => isset($input['opt_in']),
4951
]);
5052
}
5153
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Models\Reply;
6+
7+
class ReplyAbleController extends Controller
8+
{
9+
public function redirect($id, $type)
10+
{
11+
$reply = Reply::where('replyable_id', $id)->where('replyable_type', $type)->firstOrFail();
12+
13+
return redirect(route_to_reply_able($reply->replyAble));
14+
}
15+
}

app/Http/Controllers/SubscriptionController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,11 @@ public function unsubscribe(Subscribe $subscription)
1717

1818
return redirect()->route('forum.show', $thread->slug());
1919
}
20+
21+
public function redirect($id, $type)
22+
{
23+
$subscribe = Subscribe::where('subscribeable_id', $id)->where('subscribeable_type', $type)->firstOrFail();
24+
25+
return redirect(route_to_reply_able($subscribe->subscribeAble));
26+
}
2027
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Livewire;
4+
5+
use Illuminate\Support\Facades\Auth;
6+
use Livewire\Component;
7+
8+
class NotificationCount extends Component
9+
{
10+
public int $count = 0;
11+
12+
protected $listeners = [
13+
'NotificationMarkedAsRead' => 'updateCount',
14+
];
15+
16+
public function updateCount(int $count): int
17+
{
18+
return $count;
19+
}
20+
21+
public function render()
22+
{
23+
$this->count = Auth::user()->unreadNotifications()->count();
24+
25+
return view('livewire.notification-count', [
26+
'count' => $this->count,
27+
]);
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Http\Livewire;
4+
5+
use Illuminate\Support\Facades\Auth;
6+
use Livewire\Component;
7+
8+
class NotificationIndicator extends Component
9+
{
10+
public bool $hasNotification = false;
11+
12+
protected $listeners = [
13+
'NotificationMarkedAsRead' => 'setHasNotification',
14+
];
15+
16+
public function setHasNotification(int $count): bool
17+
{
18+
return $count > 0;
19+
}
20+
21+
public function render()
22+
{
23+
$this->hasNotification = $this->setHasNotification(
24+
Auth::user()->unreadNotifications()->count(),
25+
);
26+
27+
return view('livewire.notification-indicator', [
28+
'hasNotification' => $this->hasNotification,
29+
]);
30+
}
31+
}

app/Http/Livewire/Notifications.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Http\Livewire;
4+
5+
use App\Policies\NotificationPolicy;
6+
use Carbon\Carbon;
7+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
8+
use Illuminate\Notifications\DatabaseNotification;
9+
use Illuminate\Support\Facades\Auth;
10+
use Livewire\Component;
11+
use WireUi\Traits\Actions;
12+
13+
class Notifications extends Component
14+
{
15+
use Actions, AuthorizesRequests;
16+
17+
public $notificationId;
18+
19+
public function mount(): void
20+
{
21+
abort_if(Auth::guest(), 403);
22+
}
23+
24+
public function getNotificationProperty(): DatabaseNotification
25+
{
26+
return DatabaseNotification::findOrFail($this->notificationId);
27+
}
28+
29+
public function markAsRead(string $notificationId): void
30+
{
31+
$this->notificationId = $notificationId;
32+
33+
$this->authorize(NotificationPolicy::MARK_AS_READ, $this->notification);
34+
35+
$this->notification->markAsRead();
36+
37+
$this->notification()->success('Notification', 'Cette notification a été marquée comme lue.');
38+
39+
$this->emit('NotificationMarkedAsRead', Auth::user()->unreadNotifications()->count());
40+
}
41+
42+
public function render()
43+
{
44+
return view('livewire.notifications', [
45+
'notifications' => Auth::user()
46+
->unreadNotifications()
47+
->take(10)
48+
->get()
49+
->groupBy(
50+
fn ($notification) => Carbon::parse($notification->created_at)->format('M, Y')
51+
),
52+
]);
53+
}
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Http\Livewire\User\Settings;
4+
5+
use App\Models\Subscribe;
6+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
7+
use Illuminate\Support\Facades\Auth;
8+
use Livewire\Component;
9+
use WireUi\Traits\Actions;
10+
11+
class Notifications extends Component
12+
{
13+
use Actions, AuthorizesRequests;
14+
15+
public $subscribeId;
16+
17+
public function unsubscribe(string $subscribeId)
18+
{
19+
$this->subscribeId = $subscribeId;
20+
21+
$this->subscribe->delete();
22+
23+
$this->notification()->success('Désabonnement', 'Vous êtes maintenant désabonné de cet fil.');
24+
}
25+
26+
public function getSubscribeProperty(): Subscribe
27+
{
28+
return Subscribe::where('uuid', $this->subscribeId)->firstOrFail();
29+
}
30+
31+
public function render()
32+
{
33+
return view('livewire.user.settings.notifications', [
34+
'subscriptions' => Auth::user()->subscriptions,
35+
]);
36+
}
37+
}

app/Models/User.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ public function discussions(): HasMany
187187
return $this->hasMany(Discussion::class);
188188
}
189189

190+
public function subscriptions(): HasMany
191+
{
192+
return $this->hasMany(Subscribe::class);
193+
}
194+
190195
public function deleteThreads()
191196
{
192197
// We need to explicitly iterate over the threads and delete them

app/Notifications/YouWereMentioned.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function toArray($notifiable): array
3939
'author_name' => $this->reply->author->name,
4040
'author_username' => $this->reply->author->username,
4141
'author_photo' => $this->reply->author->profile_photo_url,
42+
'replyable_id' => $this->reply->replyable_id,
4243
'replyable_type' => $this->reply->replyable_type,
4344
'replyable_subject' => $this->reply->replyAble->replyAbleSubject(),
4445
];

app/Policies/NotificationPolicy.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Policies;
4+
5+
use App\Models\User;
6+
use Illuminate\Notifications\DatabaseNotification;
7+
8+
class NotificationPolicy
9+
{
10+
const MARK_AS_READ = 'markAsRead';
11+
12+
/**
13+
* Determine if the given notification can be marked as read by the user.
14+
*/
15+
public function markAsRead(User $user, DatabaseNotification $notification): bool
16+
{
17+
return $notification->notifiable->is($user);
18+
}
19+
}

app/Providers/AuthServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
use App\Models\Thread;
99
use App\Policies\ArticlePolicy;
1010
use App\Policies\DiscussionPolicy;
11+
use App\Policies\NotificationPolicy;
1112
use App\Policies\ReplyPolicy;
1213
use App\Policies\ThreadPolicy;
1314
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
15+
use Illuminate\Notifications\DatabaseNotification as Notification;
1416
use Illuminate\Support\Facades\Gate;
1517

1618
class AuthServiceProvider extends ServiceProvider
@@ -25,6 +27,7 @@ class AuthServiceProvider extends ServiceProvider
2527
Thread::class => ThreadPolicy::class,
2628
Reply::class => ReplyPolicy::class,
2729
Discussion::class => DiscussionPolicy::class,
30+
Notification::class => NotificationPolicy::class,
2831
];
2932

3033
/**

app/helpers.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,19 @@ function getFilter(string $key, array $filters = [], string $default = 'recent')
7979
return in_array($filter, $filters) ? $filter : $default;
8080
}
8181
}
82+
83+
if (! function_exists('route_to_reply_able')) {
84+
/**
85+
* Returns the route for the replyAble.
86+
*/
87+
function route_to_reply_able(mixed $replyAble)
88+
{
89+
if ($replyAble instanceof App\Models\Thread) {
90+
return route('forum.show', $replyAble->slug());
91+
}
92+
93+
if ($replyAble instanceof App\Models\Discussion) {
94+
return route('discussions.show', $replyAble->slug());
95+
}
96+
}
97+
}

public/css/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/mix-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"/js/app.js": "/js/app.js?id=9abd09e80a5426001802",
3-
"/css/app.css": "/css/app.css?id=1b6e5ebdedf4aff61229"
3+
"/css/app.css": "/css/app.css?id=f51b72c7337e6b57d1c3"
44
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@php($data = $notification->data)
2+
3+
<li>
4+
<div class="relative pb-8">
5+
<span class="absolute top-5 left-5 -ml-px h-full w-0.5 bg-skin-footer" aria-hidden="true"></span>
6+
<div class="relative flex items-start space-x-8">
7+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base">
8+
<div class="flex items-center space-x-3">
9+
<span class="p-2 flex items-center justify-center rounded-full bg-skin-card-gray">
10+
<x-heroicon-s-chat-alt class="w-5 h-5 text-skin-base"/>
11+
</span>
12+
<div>
13+
<p class="font-normal text-base leading-6">Un commentaire a été ajoutée dans la conversation <a href="{{ route('replyable', [$data['replyable_id'], $data['replyable_type']]) }}" class="text-skin-primary hover:text-skin-primary-hover">"{{ $data['replyable_subject'] }}"</a>.</p>
14+
<p class="mt-1 text-sm leading-5 text-skin-muted font-sans">
15+
<time-ago time="{{ $notification->created_at->getTimestamp() }}" />
16+
</p>
17+
</div>
18+
</div>
19+
</div>
20+
21+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base text-right">
22+
<div class="flex justify-end -mt-3">
23+
<button wire:click="markAsRead('{{ $notification->id }}')" type="button" title="Marquer comme lue" class="inline-flex items-center justify-center p-2 bg-green-500 bg-opacity-10 text-green-600 text-sm leading-5 rounded-full focus:outline-none transform hover:scale-125 transition-all">
24+
<x-heroicon-s-check class="w-5 h-5" />
25+
</button>
26+
</div>
27+
</div>
28+
</div>
29+
</div>
30+
</li>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
@php($data = $notification->data)
2+
3+
<li>
4+
<div class="relative pb-8">
5+
<span class="absolute top-5 left-5 -ml-px h-full w-0.5 bg-skin-footer" aria-hidden="true"></span>
6+
<div class="relative flex items-start space-x-8">
7+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base">
8+
<div class="flex items-center space-x-3">
9+
<span class="p-2 flex items-center justify-center rounded-full bg-skin-card-gray">
10+
<x-heroicon-s-at-symbol class="w-5 h-5 text-skin-base"/>
11+
</span>
12+
<div>
13+
<p class="font-normal text-base leading-6">
14+
<a href="{{ route('profile', $data['author_username']) }}" class="font-medium text-skin-primary">{{ $data['author_name'] }}</a>
15+
vous a mentionné dans <a href="{{ route('replyable', [$data['replyable_id'], $data['replyable_type']]) }}" class="text-skin-primary hover:text-skin-primary-hover">"{{ $data['replyable_subject'] }}"</a>.
16+
</p>
17+
<p class="mt-1 text-sm leading-5 text-skin-muted font-sans">
18+
<time-ago time="{{ $notification->created_at->getTimestamp() }}" />
19+
</p>
20+
</div>
21+
</div>
22+
</div>
23+
24+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base text-right">
25+
<div class="flex justify-end -mt-3">
26+
<button wire:click="markAsRead('{{ $notification->id }}')" type="button" title="Marquer comme lue" class="inline-flex items-center justify-center p-2 bg-green-500 bg-opacity-10 text-green-600 text-sm leading-5 rounded-full focus:outline-none transform hover:scale-125 transition-all">
27+
<x-heroicon-s-check class="w-5 h-5" />
28+
</button>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
</li>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@php($data = $notification->data)
2+
3+
<li>
4+
<div class="relative pb-8">
5+
<span class="absolute top-5 left-5 -ml-px h-full w-0.5 bg-skin-footer" aria-hidden="true"></span>
6+
<div class="relative flex items-start space-x-8">
7+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base">
8+
<div class="flex items-center space-x-3">
9+
<span class="p-2 flex items-center justify-center rounded-full bg-skin-card-gray">
10+
<x-heroicon-s-reply class="w-5 h-5 text-skin-base"/>
11+
</span>
12+
<div>
13+
<p class="font-normal text-base leading-6">Une nouvelle réponse a été ajoutée au sujet <a href="{{ route('replyable', [$data['replyable_id'], $data['replyable_type']]) }}" class="text-skin-primary hover:text-skin-primary-hover">"{{ $data['replyable_subject'] }}"</a>.</p>
14+
<p class="mt-1 text-sm leading-5 text-skin-muted font-sans">
15+
<time-ago time="{{ $notification->created_at->getTimestamp() }}" />
16+
</p>
17+
</div>
18+
</div>
19+
</div>
20+
21+
<div class="whitespace-nowrap text-sm leading-5 text-skin-base text-right">
22+
<div class="flex justify-end -mt-3">
23+
<button wire:click="markAsRead('{{ $notification->id }}')" type="button" title="Marquer comme lue" class="inline-flex items-center justify-center p-2 bg-green-500 bg-opacity-10 text-green-600 text-sm leading-5 rounded-full focus:outline-none transform hover:scale-125 transition-all">
24+
<x-heroicon-s-check class="w-5 h-5" />
25+
</button>
26+
</div>
27+
</div>
28+
</div>
29+
</div>
30+
</li>

0 commit comments

Comments
 (0)