diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index 47cd5495..9b7f4ff7 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -4,8 +4,10 @@ namespace App\Http\Controllers;
use Cache;
use Option;
+use Notification;
use Carbon\Carbon;
use App\Models\User;
+use App\Notifications;
use App\Models\Player;
use App\Models\Texture;
use Illuminate\Support\Str;
@@ -68,6 +70,38 @@ class AdminController extends Controller
];
}
+ public function sendNotification(Request $request)
+ {
+ $data = $this->validate($request, [
+ 'receiver' => 'required|in:all,normal,uid,email',
+ 'uid' => 'required_if:receiver,uid|nullable|integer|exists:users',
+ 'email' => 'required_if:receiver,email|nullable|email|exists:users',
+ 'title' => 'required|max:20',
+ 'content' => 'string|nullable',
+ ]);
+
+ $notification = new Notifications\SiteMessage($data['title'], $data['content']);
+
+ switch ($data['receiver']) {
+ case 'all':
+ $users = User::all();
+ break;
+ case 'normal':
+ $users = User::where('permission', User::NORMAL)->get();
+ break;
+ case 'uid':
+ $users = User::where('uid', $data['uid'])->get();
+ break;
+ case 'email':
+ $users = User::where('email', $data['email'])->get();
+ break;
+ }
+ Notification::send($users, $notification);
+
+ session(['sentResult' => trans('admin.notifications.send.success')]);
+ return redirect('/admin');
+ }
+
public function customize(Request $request)
{
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 00ece4e8..c67461aa 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -301,5 +301,20 @@ class UserController extends Controller
}
}
- // @codeCoverageIgnore
+ public function readNotification($id)
+ {
+ $notification = auth()
+ ->user()
+ ->unreadNotifications
+ ->first(function ($notification) use ($id) {
+ return $notification->id === $id;
+ });
+ $notification->markAsRead();
+
+ return [
+ 'title' => $notification->data['title'],
+ 'content' => app('parsedown')->text($notification->data['content']),
+ 'time' => $notification->created_at->toDateTimeString(),
+ ];
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 1c57568b..5fc6670c 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -8,10 +8,12 @@ use Illuminate\Support\Arr;
use Laravel\Passport\HasApiTokens;
use App\Events\EncryptUserPassword;
use Tymon\JWTAuth\Contracts\JWTSubject;
+use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
+ use Notifiable;
use HasApiTokens;
/**
diff --git a/app/Notifications/SiteMessage.php b/app/Notifications/SiteMessage.php
new file mode 100644
index 00000000..d62a84ae
--- /dev/null
+++ b/app/Notifications/SiteMessage.php
@@ -0,0 +1,50 @@
+title = $title;
+ $this->content = $content;
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function via($notifiable)
+ {
+ return ['database'];
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'title' => $this->title,
+ 'content' => $this->content,
+ ];
+ }
+}
diff --git a/app/Services/Hook.php b/app/Services/Hook.php
index 6b60c721..998bdb6c 100644
--- a/app/Services/Hook.php
+++ b/app/Services/Hook.php
@@ -7,6 +7,8 @@ namespace App\Services;
use Event;
use Closure;
use App\Events;
+use Notification;
+use App\Notifications;
use Illuminate\Support\Str;
class Hook
@@ -126,4 +128,9 @@ class Hook
}
);
}
+
+ public static function sendNotification($users, string $title, $content = ''): void
+ {
+ Notification::send($users, new Notifications\SiteMessage($title, $content));
+ }
}
diff --git a/config/app.php b/config/app.php
index 4699b44f..52f91ead 100644
--- a/config/app.php
+++ b/config/app.php
@@ -160,6 +160,7 @@ return [
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
+ Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
@@ -217,6 +218,7 @@ return [
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
+ 'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
diff --git a/database/migrations/2019_07_03_094434_create_notifications_table.php b/database/migrations/2019_07_03_094434_create_notifications_table.php
new file mode 100644
index 00000000..fb16d5bc
--- /dev/null
+++ b/database/migrations/2019_07_03_094434_create_notifications_table.php
@@ -0,0 +1,35 @@
+uuid('id')->primary();
+ $table->string('type');
+ $table->morphs('notifiable');
+ $table->text('data');
+ $table->timestamp('read_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('notifications');
+ }
+}
diff --git a/database/update_scripts/update-4.2.1-to-4.3.0.php b/database/update_scripts/update-4.2.1-to-4.3.0.php
new file mode 100644
index 00000000..613addeb
--- /dev/null
+++ b/database/update_scripts/update-4.2.1-to-4.3.0.php
@@ -0,0 +1,7 @@
+php artisan migrate --force',
+];
diff --git a/resources/assets/src/scripts/index.ts b/resources/assets/src/scripts/index.ts
index 213228a2..8a17db58 100644
--- a/resources/assets/src/scripts/index.ts
+++ b/resources/assets/src/scripts/index.ts
@@ -11,5 +11,6 @@ import 'admin-lte/build/js/Tree'
import './i18n'
import './net'
import './event'
+import './notification'
import './element'
import './logout'
diff --git a/resources/assets/src/scripts/notification.ts b/resources/assets/src/scripts/notification.ts
new file mode 100644
index 00000000..073a05e3
--- /dev/null
+++ b/resources/assets/src/scripts/notification.ts
@@ -0,0 +1,25 @@
+import { get } from './net'
+import { showModal } from './notify'
+
+export default async function handler(event: Event) {
+ const item = event.target as HTMLAnchorElement
+ const id = item.getAttribute('data-nid')
+ const {
+ title, content, time,
+ } = await get(`/user/notifications/${id}`)
+ showModal(`${content}
${time}`, title)
+ item.remove()
+ const counter = document.querySelector('.notifications-counter') as HTMLSpanElement
+ const value = Number.parseInt(counter.textContent!) - 1
+ if (value > 0) {
+ counter.textContent = value.toString()
+ } else {
+ counter.remove()
+ }
+}
+
+const el = document.querySelector('.notifications-list')
+// istanbul ignore next
+if (el) {
+ el.addEventListener('click', handler)
+}
diff --git a/resources/assets/tests/scripts/notification.test.ts b/resources/assets/tests/scripts/notification.test.ts
new file mode 100644
index 00000000..d7e6f21c
--- /dev/null
+++ b/resources/assets/tests/scripts/notification.test.ts
@@ -0,0 +1,34 @@
+import { get } from '@/scripts/net'
+import { showModal } from '@/scripts/notify'
+import { flushPromises } from '../utils'
+import handler from '@/scripts/notification'
+
+jest.mock('@/scripts/net')
+jest.mock('@/scripts/notify')
+
+test('read notification', async () => {
+ document.body.innerHTML = `
+