From 7a7cc2ddd9e8100d93881c30d4cdfcf463c81933 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 3 Jul 2019 16:19:13 +0800 Subject: [PATCH] Notifications --- app/Http/Controllers/AdminController.php | 34 ++++++++ app/Http/Controllers/UserController.php | 17 +++- app/Models/User.php | 2 + app/Notifications/SiteMessage.php | 50 ++++++++++++ app/Services/Hook.php | 7 ++ config/app.php | 2 + ...7_03_094434_create_notifications_table.php | 35 +++++++++ .../update_scripts/update-4.2.1-to-4.3.0.php | 7 ++ resources/assets/src/scripts/index.ts | 1 + resources/assets/src/scripts/notification.ts | 25 ++++++ .../assets/tests/scripts/notification.test.ts | 34 ++++++++ resources/lang/en/admin.yml | 13 ++++ resources/lang/en/user.yml | 1 + resources/lang/zh_CN/admin.yml | 13 ++++ resources/lang/zh_CN/user.yml | 1 + resources/misc/changelogs/en/4.3.0.md | 2 + resources/misc/changelogs/zh_CN/4.3.0.md | 2 + resources/views/admin/index.blade.php | 61 ++++++++++++++- resources/views/admin/master.blade.php | 2 + .../views/common/notifications-menu.blade.php | 30 +++++++ resources/views/skinlib/master.blade.php | 5 ++ resources/views/user/master.blade.php | 2 + routes/web.php | 2 + tests/AdminControllerTest.php | 78 +++++++++++++++++++ tests/ServicesTest/HookTest.php | 11 +++ tests/SetupControllerTest.php | 11 +-- tests/UserControllerTest.php | 19 +++++ 27 files changed, 459 insertions(+), 8 deletions(-) create mode 100644 app/Notifications/SiteMessage.php create mode 100644 database/migrations/2019_07_03_094434_create_notifications_table.php create mode 100644 database/update_scripts/update-4.2.1-to-4.3.0.php create mode 100644 resources/assets/src/scripts/notification.ts create mode 100644 resources/assets/tests/scripts/notification.test.ts create mode 100644 resources/views/common/notifications-menu.blade.php 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 = ` +
+ 2 + + +
+ ` + document.querySelector('.notifications-list')!.addEventListener('click', handler) + get.mockResolvedValue({ + title: 'title', + content: 'content', + time: 'time', + }) + + document.querySelector('a')!.click() + await flushPromises() + expect(get).toBeCalledWith('/user/notifications/1') + expect(showModal).toBeCalledWith('content
time', 'title') + expect(document.querySelectorAll('a')).toHaveLength(1) + expect(document.querySelector('span')!.textContent).toBe('1') + + document.querySelector('a')!.click() + await flushPromises() + expect(document.querySelector('span')).toBeNull() +}) diff --git a/resources/lang/en/admin.yml b/resources/lang/en/admin.yml index 9679a77e..16ceaab5 100644 --- a/resources/lang/en/admin.yml +++ b/resources/lang/en/admin.yml @@ -7,6 +7,19 @@ index: texture-uploads: Texture Uploads user-registration: User Registration +notifications: + send: + title: Send Notification + success: Sent successfully! + receiver: + title: Receiver + all: All Users + normal: Normal Users + uid: Specified UID + email: Specified Email + title: Title + content: Content (Markdown is supported.) + users: status: normal: Normal diff --git a/resources/lang/en/user.yml b/resources/lang/en/user.yml index 3ebebcc7..752f876e 100644 --- a/resources/lang/en/user.yml +++ b/resources/lang/en/user.yml @@ -13,6 +13,7 @@ cant-sign-until: You can't sign in in :time :unit last-sign: Last signed at :time sign-remain-time: Available after :time :unit announcement: Announcement +no-unread: No new notifications. verification: disabled: Email verification is not available. diff --git a/resources/lang/zh_CN/admin.yml b/resources/lang/zh_CN/admin.yml index 484f1632..efa001c1 100644 --- a/resources/lang/zh_CN/admin.yml +++ b/resources/lang/zh_CN/admin.yml @@ -7,6 +7,19 @@ index: texture-uploads: 材质上传 user-registration: 用户注册 +notifications: + send: + title: 发送通知 + success: 发送成功 + receiver: + title: 通知对象 + all: 所有用户 + normal: 普通用户 + uid: 指定 UID + email: 指定邮箱 + title: 标题 + content: 内容(可使用 Markdown) + users: status: normal: 普通用户 diff --git a/resources/lang/zh_CN/user.yml b/resources/lang/zh_CN/user.yml index d91335d9..9c0bef07 100644 --- a/resources/lang/zh_CN/user.yml +++ b/resources/lang/zh_CN/user.yml @@ -13,6 +13,7 @@ cant-sign-until: :time :unit 后才能再次签到哦 last-sign: 上次签到于 :time sign-remain-time: :time :unit 后可签到 announcement: 公告 +no-unread: 无未读通知 verification: diff --git a/resources/misc/changelogs/en/4.3.0.md b/resources/misc/changelogs/en/4.3.0.md index 2b265caa..2ea3c5a6 100644 --- a/resources/misc/changelogs/en/4.3.0.md +++ b/resources/misc/changelogs/en/4.3.0.md @@ -6,6 +6,8 @@ - Custom `PLUGINS_DIR` with relative path is allowed. - Added link for editing announcement. - New plugin API: [`Hook::addUserBadge`](https://bs-plugin.netlify.com/guide/bootstrap.html#%E6%98%BE%E7%A4%BA%E7%94%A8%E6%88%B7-badge). +- New feature: Notifications. +- New plugin API: [`Hook::sendNotification`](https://bs-plugin.netlify.com/guide/bootstrap.html#%E5%8F%91%E9%80%81%E9%80%9A%E7%9F%A5) ## Tweaked diff --git a/resources/misc/changelogs/zh_CN/4.3.0.md b/resources/misc/changelogs/zh_CN/4.3.0.md index adbcc6f2..0212da88 100644 --- a/resources/misc/changelogs/zh_CN/4.3.0.md +++ b/resources/misc/changelogs/zh_CN/4.3.0.md @@ -6,6 +6,8 @@ - 允许在 `PLUGINS_DIR` 配置项中使用相对路径 - 添加「编辑公告」的链接 - 新插件 API:[`Hook::addUserBadge`](https://bs-plugin.netlify.com/guide/bootstrap.html#%E6%98%BE%E7%A4%BA%E7%94%A8%E6%88%B7-badge) +- 新功能:发送通知。 +- 新插件 API:[`Hook::sendNotification`](https://bs-plugin.netlify.com/guide/bootstrap.html#%E5%8F%91%E9%80%81%E9%80%9A%E7%9F%A5) ## 调整 diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index 87e4ac5b..ec02a144 100644 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -69,6 +69,63 @@ + +
+
+

@lang('admin.notifications.send.title')

+
+
+ @csrf +
+ @if ($errors->any()) +
{{ $errors->first() }}
+ @endif + @if ($sentResult = Session::pull('sentResult')) +
{{ $sentResult }}
+ @endif +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
@@ -78,8 +135,8 @@
-
- + + diff --git a/resources/views/admin/master.blade.php b/resources/views/admin/master.blade.php index 63d442a4..8c0fba75 100644 --- a/resources/views/admin/master.blade.php +++ b/resources/views/admin/master.blade.php @@ -43,6 +43,8 @@