Notifications
This commit is contained in:
parent
3d88d56a9d
commit
7a7cc2ddd9
|
|
@ -4,8 +4,10 @@ namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Cache;
|
use Cache;
|
||||||
use Option;
|
use Option;
|
||||||
|
use Notification;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications;
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
use App\Models\Texture;
|
use App\Models\Texture;
|
||||||
use Illuminate\Support\Str;
|
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)
|
public function customize(Request $request)
|
||||||
{
|
{
|
||||||
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
|
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ use Illuminate\Support\Arr;
|
||||||
use Laravel\Passport\HasApiTokens;
|
use Laravel\Passport\HasApiTokens;
|
||||||
use App\Events\EncryptUserPassword;
|
use App\Events\EncryptUserPassword;
|
||||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
|
||||||
class User extends Authenticatable implements JWTSubject
|
class User extends Authenticatable implements JWTSubject
|
||||||
{
|
{
|
||||||
|
use Notifiable;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
50
app/Notifications/SiteMessage.php
Normal file
50
app/Notifications/SiteMessage.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class SiteMessage extends Notification
|
||||||
|
{
|
||||||
|
public $title;
|
||||||
|
|
||||||
|
public $content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $title, $content = '')
|
||||||
|
{
|
||||||
|
$this->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ namespace App\Services;
|
||||||
use Event;
|
use Event;
|
||||||
use Closure;
|
use Closure;
|
||||||
use App\Events;
|
use App\Events;
|
||||||
|
use Notification;
|
||||||
|
use App\Notifications;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Hook
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ return [
|
||||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||||
Illuminate\Hashing\HashServiceProvider::class,
|
Illuminate\Hashing\HashServiceProvider::class,
|
||||||
Illuminate\Mail\MailServiceProvider::class,
|
Illuminate\Mail\MailServiceProvider::class,
|
||||||
|
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||||
Illuminate\Queue\QueueServiceProvider::class,
|
Illuminate\Queue\QueueServiceProvider::class,
|
||||||
|
|
@ -217,6 +218,7 @@ return [
|
||||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||||
'Log' => Illuminate\Support\Facades\Log::class,
|
'Log' => Illuminate\Support\Facades\Log::class,
|
||||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||||
|
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||||
'Password' => Illuminate\Support\Facades\Password::class,
|
'Password' => Illuminate\Support\Facades\Password::class,
|
||||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateNotificationsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('notifications', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
7
database/update_scripts/update-4.2.1-to-4.3.0.php
Normal file
7
database/update_scripts/update-4.2.1-to-4.3.0.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'请您手动打开终端或 PowerShell 执行以下命令以完成升级:',
|
||||||
|
'Please open terminal or PowerShell to complete upgrade:',
|
||||||
|
'<code>php artisan migrate --force</code>',
|
||||||
|
];
|
||||||
|
|
@ -11,5 +11,6 @@ import 'admin-lte/build/js/Tree'
|
||||||
import './i18n'
|
import './i18n'
|
||||||
import './net'
|
import './net'
|
||||||
import './event'
|
import './event'
|
||||||
|
import './notification'
|
||||||
import './element'
|
import './element'
|
||||||
import './logout'
|
import './logout'
|
||||||
|
|
|
||||||
25
resources/assets/src/scripts/notification.ts
Normal file
25
resources/assets/src/scripts/notification.ts
Normal file
|
|
@ -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}<br><small>${time}</small>`, 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)
|
||||||
|
}
|
||||||
34
resources/assets/tests/scripts/notification.test.ts
Normal file
34
resources/assets/tests/scripts/notification.test.ts
Normal file
|
|
@ -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 = `
|
||||||
|
<div class="notifications-list">
|
||||||
|
<span class="notifications-counter">2</span>
|
||||||
|
<a data-nid="1"></a>
|
||||||
|
<a data-nid="2"></a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
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<br><small>time</small>', '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()
|
||||||
|
})
|
||||||
|
|
@ -7,6 +7,19 @@ index:
|
||||||
texture-uploads: Texture Uploads
|
texture-uploads: Texture Uploads
|
||||||
user-registration: User Registration
|
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:
|
users:
|
||||||
status:
|
status:
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ cant-sign-until: You can't sign in in :time :unit
|
||||||
last-sign: Last signed at :time
|
last-sign: Last signed at :time
|
||||||
sign-remain-time: Available after :time :unit
|
sign-remain-time: Available after :time :unit
|
||||||
announcement: Announcement
|
announcement: Announcement
|
||||||
|
no-unread: No new notifications.
|
||||||
|
|
||||||
verification:
|
verification:
|
||||||
disabled: Email verification is not available.
|
disabled: Email verification is not available.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,19 @@ index:
|
||||||
texture-uploads: 材质上传
|
texture-uploads: 材质上传
|
||||||
user-registration: 用户注册
|
user-registration: 用户注册
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
send:
|
||||||
|
title: 发送通知
|
||||||
|
success: 发送成功
|
||||||
|
receiver:
|
||||||
|
title: 通知对象
|
||||||
|
all: 所有用户
|
||||||
|
normal: 普通用户
|
||||||
|
uid: 指定 UID
|
||||||
|
email: 指定邮箱
|
||||||
|
title: 标题
|
||||||
|
content: 内容(可使用 Markdown)
|
||||||
|
|
||||||
users:
|
users:
|
||||||
status:
|
status:
|
||||||
normal: 普通用户
|
normal: 普通用户
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ cant-sign-until: :time :unit 后才能再次签到哦
|
||||||
last-sign: 上次签到于 :time
|
last-sign: 上次签到于 :time
|
||||||
sign-remain-time: :time :unit 后可签到
|
sign-remain-time: :time :unit 后可签到
|
||||||
announcement: 公告
|
announcement: 公告
|
||||||
|
no-unread: 无未读通知
|
||||||
|
|
||||||
|
|
||||||
verification:
|
verification:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
- Custom `PLUGINS_DIR` with relative path is allowed.
|
- Custom `PLUGINS_DIR` with relative path is allowed.
|
||||||
- Added link for editing announcement.
|
- 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 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
|
## Tweaked
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
- 允许在 `PLUGINS_DIR` 配置项中使用相对路径
|
- 允许在 `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::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)
|
||||||
|
|
||||||
## 调整
|
## 调整
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,63 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">@lang('admin.notifications.send.title')</h3>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url('/admin/notifications/send') }}">
|
||||||
|
@csrf
|
||||||
|
<div class="box-body">
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="callout callout-danger">{{ $errors->first() }}</div>
|
||||||
|
@endif
|
||||||
|
@if ($sentResult = Session::pull('sentResult'))
|
||||||
|
<div class="callout callout-success">{{ $sentResult }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="form-group">
|
||||||
|
<label>@lang('admin.notifications.receiver.title')</label>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="receiver" value="all" required>
|
||||||
|
@lang('admin.notifications.receiver.all')
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="receiver" value="normal" required>
|
||||||
|
@lang('admin.notifications.receiver.normal')
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="receiver" value="uid" required>
|
||||||
|
@lang('admin.notifications.receiver.uid')
|
||||||
|
<input type="number" name="uid" class="form-control">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="receiver" value="email" required>
|
||||||
|
@lang('admin.notifications.receiver.email')
|
||||||
|
<input type="email" name="email" class="form-control">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>@lang('admin.notifications.title')</label>
|
||||||
|
<input type="text" name="title" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>@lang('admin.notifications.content')</label>
|
||||||
|
<textarea name="content" class="form-control" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-footer">
|
||||||
|
<input type="submit" value="@lang('general.submit')" class="el-button el-button--primary">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
@ -78,8 +135,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
</div><!-- /.box-body -->
|
</div>
|
||||||
</div><!-- /.box -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@
|
||||||
<!-- Navbar Right Menu -->
|
<!-- Navbar Right Menu -->
|
||||||
<div class="navbar-custom-menu">
|
<div class="navbar-custom-menu">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
|
@include('common.notifications-menu')
|
||||||
|
|
||||||
@include('common.language')
|
@include('common.language')
|
||||||
|
|
||||||
@include('common.user-menu')
|
@include('common.user-menu')
|
||||||
|
|
|
||||||
30
resources/views/common/notifications-menu.blade.php
Normal file
30
resources/views/common/notifications-menu.blade.php
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
@php
|
||||||
|
$notifications = $user->unreadNotifications;
|
||||||
|
$count = $notifications->count();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<li class="dropdown notifications-menu">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
@if ($count > 0)
|
||||||
|
<span class="label label-warning notifications-counter">{{ $count }}</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
@if ($count === 0)
|
||||||
|
<li class="header text-center">@lang('user.no-unread')</li>
|
||||||
|
@else
|
||||||
|
<li>
|
||||||
|
<ul class="menu notifications-list">
|
||||||
|
@foreach ($notifications as $notification)
|
||||||
|
<li>
|
||||||
|
<a href="#" data-nid="{{ $notification->id }}">
|
||||||
|
<i class="far fa-circle text-aqua"></i> {{ $notification->data['title'] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
@ -37,15 +37,20 @@
|
||||||
<li class="active">
|
<li class="active">
|
||||||
<a href="{{ url('skinlib') }}">@lang('general.skinlib')</a>
|
<a href="{{ url('skinlib') }}">@lang('general.skinlib')</a>
|
||||||
</li>
|
</li>
|
||||||
|
@auth
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url('user/closet') }}">@lang('general.my-closet')</a>
|
<a href="{{ url('user/closet') }}">@lang('general.my-closet')</a>
|
||||||
</li>
|
</li>
|
||||||
|
@endauth
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- /.navbar-collapse -->
|
</div><!-- /.navbar-collapse -->
|
||||||
<!-- Navbar Right Menu -->
|
<!-- Navbar Right Menu -->
|
||||||
<div class="navbar-custom-menu">
|
<div class="navbar-custom-menu">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
|
@auth
|
||||||
<li><a href="{{ url('skinlib/upload') }}"><i class="fas fa-upload" aria-hidden="true"></i> <span class="description-text">@lang('skinlib.general.upload-new-skin')</span></a></li>
|
<li><a href="{{ url('skinlib/upload') }}"><i class="fas fa-upload" aria-hidden="true"></i> <span class="description-text">@lang('skinlib.general.upload-new-skin')</span></a></li>
|
||||||
|
@include('common.notifications-menu')
|
||||||
|
@endauth
|
||||||
|
|
||||||
@include('common.language')
|
@include('common.language')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@
|
||||||
<!-- Navbar Right Menu -->
|
<!-- Navbar Right Menu -->
|
||||||
<div class="navbar-custom-menu">
|
<div class="navbar-custom-menu">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
|
@include('common.notifications-menu')
|
||||||
|
|
||||||
@include('common.language')
|
@include('common.language')
|
||||||
|
|
||||||
@include('common.user-menu')
|
@include('common.user-menu')
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ Route::group([
|
||||||
'prefix' => 'user',
|
'prefix' => 'user',
|
||||||
], function () {
|
], function () {
|
||||||
Route::any('', 'UserController@index');
|
Route::any('', 'UserController@index');
|
||||||
|
Route::get('/notifications/{id}', 'UserController@readNotification');
|
||||||
Route::get('/score-info', 'UserController@scoreInfo');
|
Route::get('/score-info', 'UserController@scoreInfo');
|
||||||
Route::post('/sign', 'UserController@sign');
|
Route::post('/sign', 'UserController@sign');
|
||||||
|
|
||||||
|
|
@ -115,6 +116,7 @@ Route::group(['prefix' => 'skinlib'], function () {
|
||||||
Route::group(['middleware' => ['authorize', 'admin'], 'prefix' => 'admin'], function () {
|
Route::group(['middleware' => ['authorize', 'admin'], 'prefix' => 'admin'], function () {
|
||||||
Route::view('/', 'admin.index');
|
Route::view('/', 'admin.index');
|
||||||
Route::get('/chart', 'AdminController@chartData');
|
Route::get('/chart', 'AdminController@chartData');
|
||||||
|
Route::post('/notifications/send', 'AdminController@sendNotification');
|
||||||
|
|
||||||
Route::any('/customize', 'AdminController@customize');
|
Route::any('/customize', 'AdminController@customize');
|
||||||
Route::any('/score', 'AdminController@score');
|
Route::any('/score', 'AdminController@score');
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
|
use Notification;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications;
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
use App\Models\Texture;
|
use App\Models\Texture;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -32,7 +34,83 @@ class AdminControllerTest extends TestCase
|
||||||
->assertJsonStructure(['labels', 'xAxis', 'data']);
|
->assertJsonStructure(['labels', 'xAxis', 'data']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSendNotification()
|
||||||
|
{
|
||||||
|
$admin = factory(User::class, 'admin')->create();
|
||||||
|
$normal = factory(User::class)->create();
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->actingAs($admin)
|
||||||
|
->post('/admin/notifications/send', [
|
||||||
|
'receiver' => 'all',
|
||||||
|
'title' => 'all users',
|
||||||
|
'content' => null,
|
||||||
|
])
|
||||||
|
->assertRedirect('/admin')
|
||||||
|
->assertSessionHas('sentResult', trans('admin.notifications.send.success'));
|
||||||
|
Notification::assertSentTo(
|
||||||
|
[$admin, $normal],
|
||||||
|
Notifications\SiteMessage::class,
|
||||||
|
function ($notification) {
|
||||||
|
$this->assertEquals('all users', $notification->title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Notification::fake();
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
$this->post('/admin/notifications/send', [
|
||||||
|
'receiver' => 'normal',
|
||||||
|
'title' => 'normal only',
|
||||||
|
'content' => 'hi',
|
||||||
|
]);
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$normal,
|
||||||
|
Notifications\SiteMessage::class,
|
||||||
|
function ($notification) {
|
||||||
|
$this->assertEquals('normal only', $notification->title);
|
||||||
|
$this->assertEquals('hi', $notification->content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Notification::assertNotSentTo($admin, Notifications\SiteMessage::class);
|
||||||
|
|
||||||
|
Notification::fake();
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
$this->post('/admin/notifications/send', [
|
||||||
|
'receiver' => 'uid',
|
||||||
|
'title' => 'uid',
|
||||||
|
'content' => null,
|
||||||
|
'uid' => $normal->uid,
|
||||||
|
]);
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$normal,
|
||||||
|
Notifications\SiteMessage::class,
|
||||||
|
function ($notification) {
|
||||||
|
$this->assertEquals('uid', $notification->title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Notification::assertNotSentTo($admin, Notifications\SiteMessage::class);
|
||||||
|
|
||||||
|
Notification::fake();
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
$this->post('/admin/notifications/send', [
|
||||||
|
'receiver' => 'email',
|
||||||
|
'title' => 'email',
|
||||||
|
'content' => null,
|
||||||
|
'email' => $normal->email,
|
||||||
|
]);
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$normal,
|
||||||
|
Notifications\SiteMessage::class,
|
||||||
|
function ($notification) {
|
||||||
|
$this->assertEquals('email', $notification->title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Notification::assertNotSentTo($admin, Notifications\SiteMessage::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function testUsers()
|
public function testUsers()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Hook;
|
use App\Services\Hook;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Tests\Concerns\GeneratesFakePlugins;
|
use Tests\Concerns\GeneratesFakePlugins;
|
||||||
|
|
@ -92,4 +93,14 @@ class HookTest extends TestCase
|
||||||
->get('/user')
|
->get('/user')
|
||||||
->assertSee('<small class="label bg-green">hi</small>');
|
->assertSee('<small class="label bg-green">hi</small>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSendNotification()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
Hook::sendNotification([$user], 'Ibara Mayaka');
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get('/user')
|
||||||
|
->assertSee('<span class="label label-warning">1</span>')
|
||||||
|
->assertSee('Ibara Mayaka');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,12 @@ class SetupControllerTest extends TestCase
|
||||||
'textures',
|
'textures',
|
||||||
'users',
|
'users',
|
||||||
'reports',
|
'reports',
|
||||||
|
'oauth_auth_codes',
|
||||||
|
'oauth_access_tokens',
|
||||||
|
'oauth_clients',
|
||||||
|
'oauth_personal_access_clients',
|
||||||
|
'oauth_refresh_tokens',
|
||||||
|
'notifications',
|
||||||
];
|
];
|
||||||
array_walk($tables, function ($table) {
|
array_walk($tables, function ($table) {
|
||||||
Schema::dropIfExists($table);
|
Schema::dropIfExists($table);
|
||||||
|
|
@ -84,11 +90,6 @@ class SetupControllerTest extends TestCase
|
||||||
public function testInfo()
|
public function testInfo()
|
||||||
{
|
{
|
||||||
$this->get('/setup/info')->assertViewIs('setup.wizard.info');
|
$this->get('/setup/info')->assertViewIs('setup.wizard.info');
|
||||||
Schema::dropIfExists('oauth_auth_codes');
|
|
||||||
Schema::dropIfExists('oauth_access_tokens');
|
|
||||||
Schema::dropIfExists('oauth_clients');
|
|
||||||
Schema::dropIfExists('oauth_personal_access_clients');
|
|
||||||
Schema::dropIfExists('oauth_refresh_tokens');
|
|
||||||
Artisan::call('migrate:refresh');
|
Artisan::call('migrate:refresh');
|
||||||
Schema::drop('users');
|
Schema::drop('users');
|
||||||
$this->get('/setup/info')->assertSee('already exist');
|
$this->get('/setup/info')->assertSee('already exist');
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use Event;
|
||||||
use Parsedown;
|
use Parsedown;
|
||||||
use App\Events;
|
use App\Events;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Mail\EmailVerification;
|
use App\Mail\EmailVerification;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
@ -490,4 +491,22 @@ class UserControllerTest extends TestCase
|
||||||
->assertJson(['code' => 0]);
|
->assertJson(['code' => 0]);
|
||||||
$this->assertEquals(0, User::find($user->uid)->avatar);
|
$this->assertEquals(0, User::find($user->uid)->avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReadNotification()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
$user->notify(new Notifications\SiteMessage('Hyouka', 'Kotenbu?'));
|
||||||
|
$user->refresh();
|
||||||
|
$notification = $user->unreadNotifications->first();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get('/user/notifications/'.$notification->id)
|
||||||
|
->assertJson([
|
||||||
|
'title' => $notification->data['title'],
|
||||||
|
'content' => app('parsedown')->text($notification->data['content']),
|
||||||
|
'time' => $notification->created_at->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
$notification->refresh();
|
||||||
|
$this->assertNotNull($notification->read_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user