Browse Source

Merge branch 'message-notification' into dev

moell 11 months ago
parent
commit
c3572ecb00
36 changed files with 1039 additions and 18 deletions
  1. 46 0
      app/Events/ObjectActionCreate.php
  2. 3 15
      app/Http/Requests/API/Config/AllowSettingConfig.php
  3. 68 0
      app/Listeners/SendActionBrowserNotification.php
  4. 40 0
      app/Listeners/SendActionEmailNotification.php
  5. 56 0
      app/Mail/RequirementAction.php
  6. 56 0
      app/Mail/TaskAction.php
  7. 1 1
      app/Models/Enums/ActionObjectType.php
  8. 10 0
      app/Models/Enums/NotificationObjectType.php
  9. 13 0
      app/Models/Notification.php
  10. 13 0
      app/Models/NotificationRecord.php
  11. 8 1
      app/Providers/EventServiceProvider.php
  12. 4 1
      app/Repositories/ActionRepository.php
  13. 36 0
      app/Repositories/ConfigRepository.php
  14. 41 0
      app/Repositories/Enums/EmailConfigFieldEnum.php
  15. 69 0
      app/Services/Notification/ActionEmail/ActionEmailService.php
  16. 34 0
      database/migrations/2024_04_07_211410_create_notifications_table.php
  17. 30 0
      database/migrations/2024_04_08_221714_create_notification_records_table.php
  18. 12 0
      resources/views/emails/actions/requirement.blade.php
  19. 12 0
      resources/views/emails/actions/task.blade.php
  20. 24 0
      resources/views/vendor/mail/html/button.blade.php
  21. 11 0
      resources/views/vendor/mail/html/footer.blade.php
  22. 12 0
      resources/views/vendor/mail/html/header.blade.php
  23. 57 0
      resources/views/vendor/mail/html/layout.blade.php
  24. 27 0
      resources/views/vendor/mail/html/message.blade.php
  25. 14 0
      resources/views/vendor/mail/html/panel.blade.php
  26. 7 0
      resources/views/vendor/mail/html/subcopy.blade.php
  27. 3 0
      resources/views/vendor/mail/html/table.blade.php
  28. 290 0
      resources/views/vendor/mail/html/themes/default.css
  29. 1 0
      resources/views/vendor/mail/text/button.blade.php
  30. 1 0
      resources/views/vendor/mail/text/footer.blade.php
  31. 1 0
      resources/views/vendor/mail/text/header.blade.php
  32. 9 0
      resources/views/vendor/mail/text/layout.blade.php
  33. 27 0
      resources/views/vendor/mail/text/message.blade.php
  34. 1 0
      resources/views/vendor/mail/text/panel.blade.php
  35. 1 0
      resources/views/vendor/mail/text/subcopy.blade.php
  36. 1 0
      resources/views/vendor/mail/text/table.blade.php

+ 46 - 0
app/Events/ObjectActionCreate.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Events;
+
+use App\Models\Action;
+use App\Models\Config;
+use App\Models\Enums\ConfigGroup;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ObjectActionCreate
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public array $notificationSetting = [];
+
+    /**
+     * Create a new event instance.
+     */
+    public function __construct(public Action $action)
+    {
+        $settingResult = Config::query()
+            ->where("group", ConfigGroup::MESSAGE_NOTIFICATION)
+            ->where("key", "setting")
+            ->first();
+
+        $this->notificationSetting = $settingResult ? json_decode($settingResult->value, true) : [];
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     *
+     * @return array<int, \Illuminate\Broadcasting\Channel>
+     */
+    public function broadcastOn(): array
+    {
+        return [
+            new PrivateChannel('channel-name'),
+        ];
+    }
+}

+ 3 - 15
app/Http/Requests/API/Config/AllowSettingConfig.php

@@ -2,6 +2,8 @@
 
 namespace App\Http\Requests\API\Config;
 
+use App\Repositories\Enums\EmailConfigFieldEnum;
+
 class AllowSettingConfig
 {
     public function check(string $group, string $key, mixed $value)
@@ -29,20 +31,6 @@ class AllowSettingConfig
      */
     private function email(): array
     {
-        return [
-            "email_notification" => "in:on,off",
-            "async_sender" => "in:yes,no",
-            "sender_email" => "nullable|email",
-            "sender" => "nullable|min:1",
-            "domain" => "nullable|url",
-            "smtp_server" => "nullable|min:1",
-            "smtp_account" => "nullable|min:1",
-            "smtp_validation" => "in:yes,no",
-            "smtp_port" => "nullable|numeric",
-            "encryption" => "in:ssl,plain,tls",
-            "smtp_password" => "nullable|min:6",
-            "debug" => "in:off,normal,high",
-            "charset" => "in:utf8,gbk"
-        ];
+        return EmailConfigFieldEnum::checkRules();
     }
 }

+ 68 - 0
app/Listeners/SendActionBrowserNotification.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Events\ObjectActionCreate;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\NotificationObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Notification;
+use App\Models\NotificationRecord;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
+
+class SendActionBrowserNotification implements ShouldQueue
+{
+    /**
+     * Create the event listener.
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Handle the event.
+     */
+    public function handle(ObjectActionCreate $event): void
+    {
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+
+        $object = $actionObjectType->modelBuilder()->find($event->action->object_id);
+        if (! $object) {
+            return;
+        }
+
+        $userIds = match ($actionObjectType) {
+            ActionObjectType::TASK => $object->assign > 0 ? [$object->assign] : [],
+            default => [],
+        };
+
+        if (! $userIds) {
+            return;
+        }
+
+        $notification = Notification::query()->create([
+            'object_type' => NotificationObjectType::ACTION->value,
+            'object_id' => $event->action->id,
+        ]);
+
+        foreach ($userIds as $userId) {
+            NotificationRecord::query()->create([
+                'notification_id' => $notification->id,
+                'user_id' => $userId,
+            ]);
+        }
+    }
+
+    public function shouldQueue(ObjectActionCreate $event): bool
+    {
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+        $objectAction = ObjectAction::tryFrom($event->action->action);
+        if (! $actionObjectType || !$objectAction) {
+            return false;
+        }
+
+        return in_array($objectAction->value, $event->notificationSetting[$actionObjectType->value]['email'] ?? []);
+    }
+}

+ 40 - 0
app/Listeners/SendActionEmailNotification.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Events\ObjectActionCreate;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Services\Notification\ActionEmail\ActionEmailService;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
+
+class SendActionEmailNotification implements ShouldQueue
+{
+    /**
+     * Create the event listener.
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Handle the event.
+     */
+    public function handle(ObjectActionCreate $event): void
+    {
+        (new ActionEmailService($event->action))->send();
+    }
+
+    public function shouldQueue(ObjectActionCreate $event): bool
+    {
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+        $objectAction = ObjectAction::tryFrom($event->action->action);
+        if (! $actionObjectType || !$objectAction) {
+            return false;
+        }
+
+        return in_array($objectAction->value, $event->notificationSetting[$actionObjectType->value]['email'] ?? []);
+    }
+}

+ 56 - 0
app/Mail/RequirementAction.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\Enums\ObjectAction;
+use App\Models\Requirement;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class RequirementAction extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(protected  Requirement $requirement, protected ObjectAction $objectAction)
+    {
+        //
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Requirement Action',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            //view: 'view.name',
+            markdown: 'emails.actions.requirement',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 56 - 0
app/Mail/TaskAction.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\Enums\ObjectAction;
+use App\Models\Task;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class TaskAction extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(protected Task $task, protected ObjectAction $objectAction)
+    {
+        //
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Task Action',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            //view: 'view.name',
+            markdown: 'emails.actions.task',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 1 - 1
app/Models/Enums/ActionObjectType.php

@@ -17,7 +17,7 @@ enum ActionObjectType: string
 
     case PROJECT = "project";
 
-    case REQUIREMENT="requirement";
+    case REQUIREMENT = "requirement";
 
     case TASK = "task";
 

+ 10 - 0
app/Models/Enums/NotificationObjectType.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum NotificationObjectType: string
+{
+    case ANNOUNCEMENT = "announcement"; //公告
+
+    case ACTION = "action"; //Action 通知
+}

+ 13 - 0
app/Models/Notification.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Notification extends Model
+{
+    use HasFactory;
+
+    protected $guarded = ['id'];
+}

+ 13 - 0
app/Models/NotificationRecord.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class NotificationRecord extends Model
+{
+    use HasFactory;
+
+    protected $guarded = ['id'];
+}

+ 8 - 1
app/Providers/EventServiceProvider.php

@@ -2,6 +2,9 @@
 
 namespace App\Providers;
 
+use App\Events\ObjectActionCreate;
+use App\Listeners\SendActionBrowserNotification;
+use App\Listeners\SendActionEmailNotification;
 use Illuminate\Auth\Events\Registered;
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -18,6 +21,10 @@ class EventServiceProvider extends ServiceProvider
         Registered::class => [
             SendEmailVerificationNotification::class,
         ],
+        ObjectActionCreate::class => [
+            SendActionBrowserNotification::class,
+            SendActionEmailNotification::class,
+        ]
     ];
 
     /**
@@ -25,7 +32,7 @@ class EventServiceProvider extends ServiceProvider
      */
     public function boot(): void
     {
-        //
+
     }
 
     /**

+ 4 - 1
app/Repositories/ActionRepository.php

@@ -2,6 +2,7 @@
 
 namespace App\Repositories;
 
+use App\Events\ObjectActionCreate;
 use App\Models\Action;
 use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
@@ -34,7 +35,7 @@ class ActionRepository
         $action = Action::query()->create([
             "object_id" => $objectId,
             "object_type" => $objectType->value,
-            "action" => $action,
+            "action" => $action->value,
             "project_id" => $projectId,
             "comment" => $comment,
             "extra_fields" => $extraFields ?: null,
@@ -45,6 +46,8 @@ class ActionRepository
             History::query()->insert(array_map(fn($change) => [...$change, 'action_id' => $action->id], $objectChanges));
         }
 
+        ObjectActionCreate::dispatch($action);
+
         return $action;
     }
 

+ 36 - 0
app/Repositories/ConfigRepository.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Repositories;
+
+use App\Models\Config;
+use App\Models\Enums\ConfigGroup;
+use App\Repositories\Enums\EmailConfigFieldEnum;
+
+class ConfigRepository
+{
+    public static function openEmailNotification(): bool
+    {
+        return self::getConfigItem(ConfigGroup::EMAIL->value, EmailConfigFieldEnum::OPEN_EMAIL_NOTIFICATION->value) == "on";
+    }
+
+    protected static function getConfigItem(string $group, string $key)
+    {
+        $config = Config::query()->where('group', $group)->where("key", $key)->first();
+
+        return $config?->value;
+    }
+
+    public static function emailDynamicSetting()
+    {
+        $configs = Config::query()->where('group', ConfigGroup::EMAIL->value)->get();
+
+        $fieldRelations = EmailConfigFieldEnum::fieldRelations();
+        foreach ($configs as $config) {
+            if(! isset($fieldRelations[$config->key]) || !$config->value) {
+                continue;
+            }
+
+            \Illuminate\Support\Facades\Config::set($fieldRelations[$config->key], $config->value);
+        }
+    }
+}

+ 41 - 0
app/Repositories/Enums/EmailConfigFieldEnum.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Repositories\Enums;
+
+enum EmailConfigFieldEnum: string
+{
+    case OPEN_EMAIL_NOTIFICATION = "email_notification"; //邮件通知
+
+    public static function checkRules(): array
+    {
+        return [
+            "email_notification" => "in:on,off",
+            "async_sender" => "in:yes,no",
+            "sender_email" => "nullable|email",
+            "sender" => "nullable|min:1",
+            "domain" => "nullable|url",
+            "smtp_server" => "nullable|min:1",
+            "smtp_account" => "nullable|min:1",
+            "smtp_validation" => "in:yes,no",
+            "smtp_port" => "nullable|numeric",
+            "encryption" => "in:ssl,plain,tls",
+            "smtp_password" => "nullable|min:6",
+            "debug" => "in:off,normal,high",
+            "charset" => "in:utf8,gbk"
+        ];
+    }
+
+    public static function fieldRelations(): array
+    {
+        return [
+            "sender_email" => "mail.from.address",
+            "sender" => "mail.from.name",
+            "domain" => "mail.mailers.smtp.url",
+            "smtp_server" => "mail.mailers.smtp.host",
+            "smtp_account" => "mail.mailers.smtp.username",
+            "smtp_port" => "mail.mailers.smtp.port",
+            "encryption" => "mail.mailers.smtp.encryption",
+            "smtp_password" => "mail.mailers.smtp.password",
+        ];
+    }
+}

+ 69 - 0
app/Services/Notification/ActionEmail/ActionEmailService.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Services\Notification\ActionEmail;
+
+use App\Mail\RequirementAction;
+use App\Mail\TaskAction;
+use App\Models\Action;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Requirement;
+use App\Models\Task;
+use App\Models\User;
+use App\Repositories\ConfigRepository;
+use Illuminate\Contracts\Mail\Mailable;
+use Illuminate\Support\Facades\Mail;
+
+class ActionEmailService
+{
+    protected ObjectAction $objectAction;
+
+    public function __construct(
+        protected Action $action
+    )
+    {
+        $this->objectAction = ObjectAction::tryFrom($this->action->action);
+    }
+
+    public function send()
+    {
+        if (! ConfigRepository::openEmailNotification()) {
+            return;
+        }
+
+        ConfigRepository::emailDynamicSetting();
+
+        $actionObjectType = ActionObjectType::tryFrom($this->action->object_type);
+
+        $actionObjectModel = $actionObjectType->modelBuilder()->find($this->action->object_id);
+
+        match ($actionObjectType) {
+            ActionObjectType::REQUIREMENT => $this->requirement($actionObjectModel),
+            ActionObjectType::TASK => $this->task($actionObjectModel),
+        };
+    }
+
+    protected function requirement(Requirement $requirement)
+    {
+        $this->dispatch($requirement->mailto, new RequirementAction($requirement, $this->objectAction));
+    }
+
+    protected function task(Task $task)
+    {
+        $userIds = array_filter([$task->assign, ...$task->mailto]);
+
+        $this->dispatch($userIds, new TaskAction($task, $this->objectAction));
+    }
+
+    protected function dispatch(array $userIds, Mailable $mailable)
+    {
+        $users = User::query()->whereIn("id", $userIds)->get();
+        if ($users->isEmpty()) {
+            return;
+        }
+
+        foreach ($users as $user) {
+            Mail::to($user)->send($mailable);
+        }
+    }
+}

+ 34 - 0
database/migrations/2024_04_07_211410_create_notifications_table.php

@@ -0,0 +1,34 @@
+<?php
+
+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::create('notifications', function (Blueprint $table) {
+            $table->id();
+            $table->string('object_type', 30)->comment("action;announcement");
+            $table->integer('object_id')->default(0);
+            $table->text('content')->nullable();
+            $table->timestamp("start_at")->nullable();
+            $table->timestamp("end_at")->nullable();
+            $table->integer("created_by")->nullable();
+            $table->string("status", 30)->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('notifications');
+    }
+};

+ 30 - 0
database/migrations/2024_04_08_221714_create_notification_records_table.php

@@ -0,0 +1,30 @@
+<?php
+
+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::create('notification_records', function (Blueprint $table) {
+            $table->id();
+            $table->integer("notification_id");
+            $table->integer("user_id");
+            $table->timestamp("read_at")->nullable()->default(null);
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('notification_records');
+    }
+};

+ 12 - 0
resources/views/emails/actions/requirement.blade.php

@@ -0,0 +1,12 @@
+<x-mail::message>
+# Introduction
+
+The body of your message.
+
+<x-mail::button :url="''">
+Button Text
+</x-mail::button>
+
+Thanks,<br>
+{{ config('app.name') }}
+</x-mail::message>

+ 12 - 0
resources/views/emails/actions/task.blade.php

@@ -0,0 +1,12 @@
+<x-mail::message>
+# Introduction
+
+The body of your message.
+
+<x-mail::button :url="''">
+Button Text
+</x-mail::button>
+
+Thanks,<br>
+{{ config('app.name') }}
+</x-mail::message>

+ 24 - 0
resources/views/vendor/mail/html/button.blade.php

@@ -0,0 +1,24 @@
+@props([
+    'url',
+    'color' => 'primary',
+    'align' => 'center',
+])
+<table class="action" align="{{ $align }}" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td align="{{ $align }}">
+<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td align="{{ $align }}">
+<table border="0" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td>
+<a href="{{ $url }}" class="button button-{{ $color }}" target="_blank" rel="noopener">{{ $slot }}</a>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+</table>

+ 11 - 0
resources/views/vendor/mail/html/footer.blade.php

@@ -0,0 +1,11 @@
+<tr>
+<td>
+<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td class="content-cell" align="center">
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+</td>
+</tr>
+</table>
+</td>
+</tr>

+ 12 - 0
resources/views/vendor/mail/html/header.blade.php

@@ -0,0 +1,12 @@
+@props(['url'])
+<tr>
+<td class="header">
+<a href="{{ $url }}" style="display: inline-block;">
+@if (trim($slot) === 'Laravel')
+<img src="https://laravel.com/img/notification-logo.png" class="logo" alt="Laravel Logo">
+@else
+{{ $slot }}
+@endif
+</a>
+</td>
+</tr>

+ 57 - 0
resources/views/vendor/mail/html/layout.blade.php

@@ -0,0 +1,57 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>{{ config('app.name') }}</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<meta name="color-scheme" content="light">
+<meta name="supported-color-schemes" content="light">
+<style>
+@media only screen and (max-width: 600px) {
+.inner-body {
+width: 100% !important;
+}
+
+.footer {
+width: 100% !important;
+}
+}
+
+@media only screen and (max-width: 500px) {
+.button {
+width: 100% !important;
+}
+}
+</style>
+</head>
+<body>
+
+<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td align="center">
+<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+{{ $header ?? '' }}
+
+<!-- Email Body -->
+<tr>
+<td class="body" width="100%" cellpadding="0" cellspacing="0" style="border: hidden !important;">
+<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
+<!-- Body content -->
+<tr>
+<td class="content-cell">
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+
+{{ $subcopy ?? '' }}
+</td>
+</tr>
+</table>
+</td>
+</tr>
+
+{{ $footer ?? '' }}
+</table>
+</td>
+</tr>
+</table>
+</body>
+</html>

+ 27 - 0
resources/views/vendor/mail/html/message.blade.php

@@ -0,0 +1,27 @@
+<x-mail::layout>
+{{-- Header --}}
+<x-slot:header>
+<x-mail::header :url="config('app.url')">
+{{ config('app.name') }}
+</x-mail::header>
+</x-slot:header>
+
+{{-- Body --}}
+{{ $slot }}
+
+{{-- Subcopy --}}
+@isset($subcopy)
+<x-slot:subcopy>
+<x-mail::subcopy>
+{{ $subcopy }}
+</x-mail::subcopy>
+</x-slot:subcopy>
+@endisset
+
+{{-- Footer --}}
+<x-slot:footer>
+<x-mail::footer>
+© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
+</x-mail::footer>
+</x-slot:footer>
+</x-mail::layout>

+ 14 - 0
resources/views/vendor/mail/html/panel.blade.php

@@ -0,0 +1,14 @@
+<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td class="panel-content">
+<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td class="panel-item">
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+</td>
+</tr>
+</table>
+</td>
+</tr>
+</table>
+

+ 7 - 0
resources/views/vendor/mail/html/subcopy.blade.php

@@ -0,0 +1,7 @@
+<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+<tr>
+<td>
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+</td>
+</tr>
+</table>

+ 3 - 0
resources/views/vendor/mail/html/table.blade.php

@@ -0,0 +1,3 @@
+<div class="table">
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+</div>

+ 290 - 0
resources/views/vendor/mail/html/themes/default.css

@@ -0,0 +1,290 @@
+/* Base */
+
+body,
+body *:not(html):not(style):not(br):not(tr):not(code) {
+    box-sizing: border-box;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
+        'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+    position: relative;
+}
+
+body {
+    -webkit-text-size-adjust: none;
+    background-color: #ffffff;
+    color: #718096;
+    height: 100%;
+    line-height: 1.4;
+    margin: 0;
+    padding: 0;
+    width: 100% !important;
+}
+
+p,
+ul,
+ol,
+blockquote {
+    line-height: 1.4;
+    text-align: left;
+}
+
+a {
+    color: #3869d4;
+}
+
+a img {
+    border: none;
+}
+
+/* Typography */
+
+h1 {
+    color: #3d4852;
+    font-size: 18px;
+    font-weight: bold;
+    margin-top: 0;
+    text-align: left;
+}
+
+h2 {
+    font-size: 16px;
+    font-weight: bold;
+    margin-top: 0;
+    text-align: left;
+}
+
+h3 {
+    font-size: 14px;
+    font-weight: bold;
+    margin-top: 0;
+    text-align: left;
+}
+
+p {
+    font-size: 16px;
+    line-height: 1.5em;
+    margin-top: 0;
+    text-align: left;
+}
+
+p.sub {
+    font-size: 12px;
+}
+
+img {
+    max-width: 100%;
+}
+
+/* Layout */
+
+.wrapper {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 100%;
+    background-color: #edf2f7;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+}
+
+.content {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 100%;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+}
+
+/* Header */
+
+.header {
+    padding: 25px 0;
+    text-align: center;
+}
+
+.header a {
+    color: #3d4852;
+    font-size: 19px;
+    font-weight: bold;
+    text-decoration: none;
+}
+
+/* Logo */
+
+.logo {
+    height: 75px;
+    max-height: 75px;
+    width: 75px;
+}
+
+/* Body */
+
+.body {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 100%;
+    background-color: #edf2f7;
+    border-bottom: 1px solid #edf2f7;
+    border-top: 1px solid #edf2f7;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+}
+
+.inner-body {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 570px;
+    background-color: #ffffff;
+    border-color: #e8e5ef;
+    border-radius: 2px;
+    border-width: 1px;
+    box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);
+    margin: 0 auto;
+    padding: 0;
+    width: 570px;
+}
+
+/* Subcopy */
+
+.subcopy {
+    border-top: 1px solid #e8e5ef;
+    margin-top: 25px;
+    padding-top: 25px;
+}
+
+.subcopy p {
+    font-size: 14px;
+}
+
+/* Footer */
+
+.footer {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 570px;
+    margin: 0 auto;
+    padding: 0;
+    text-align: center;
+    width: 570px;
+}
+
+.footer p {
+    color: #b0adc5;
+    font-size: 12px;
+    text-align: center;
+}
+
+.footer a {
+    color: #b0adc5;
+    text-decoration: underline;
+}
+
+/* Tables */
+
+.table table {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 100%;
+    margin: 30px auto;
+    width: 100%;
+}
+
+.table th {
+    border-bottom: 1px solid #edeff2;
+    margin: 0;
+    padding-bottom: 8px;
+}
+
+.table td {
+    color: #74787e;
+    font-size: 15px;
+    line-height: 18px;
+    margin: 0;
+    padding: 10px 0;
+}
+
+.content-cell {
+    max-width: 100vw;
+    padding: 32px;
+}
+
+/* Buttons */
+
+.action {
+    -premailer-cellpadding: 0;
+    -premailer-cellspacing: 0;
+    -premailer-width: 100%;
+    margin: 30px auto;
+    padding: 0;
+    text-align: center;
+    width: 100%;
+}
+
+.button {
+    -webkit-text-size-adjust: none;
+    border-radius: 4px;
+    color: #fff;
+    display: inline-block;
+    overflow: hidden;
+    text-decoration: none;
+}
+
+.button-blue,
+.button-primary {
+    background-color: #2d3748;
+    border-bottom: 8px solid #2d3748;
+    border-left: 18px solid #2d3748;
+    border-right: 18px solid #2d3748;
+    border-top: 8px solid #2d3748;
+}
+
+.button-green,
+.button-success {
+    background-color: #48bb78;
+    border-bottom: 8px solid #48bb78;
+    border-left: 18px solid #48bb78;
+    border-right: 18px solid #48bb78;
+    border-top: 8px solid #48bb78;
+}
+
+.button-red,
+.button-error {
+    background-color: #e53e3e;
+    border-bottom: 8px solid #e53e3e;
+    border-left: 18px solid #e53e3e;
+    border-right: 18px solid #e53e3e;
+    border-top: 8px solid #e53e3e;
+}
+
+/* Panels */
+
+.panel {
+    border-left: #2d3748 solid 4px;
+    margin: 21px 0;
+}
+
+.panel-content {
+    background-color: #edf2f7;
+    color: #718096;
+    padding: 16px;
+}
+
+.panel-content p {
+    color: #718096;
+}
+
+.panel-item {
+    padding: 0;
+}
+
+.panel-item p:last-of-type {
+    margin-bottom: 0;
+    padding-bottom: 0;
+}
+
+/* Utilities */
+
+.break-all {
+    word-break: break-all;
+}

+ 1 - 0
resources/views/vendor/mail/text/button.blade.php

@@ -0,0 +1 @@
+{{ $slot }}: {{ $url }}

+ 1 - 0
resources/views/vendor/mail/text/footer.blade.php

@@ -0,0 +1 @@
+{{ $slot }}

+ 1 - 0
resources/views/vendor/mail/text/header.blade.php

@@ -0,0 +1 @@
+[{{ $slot }}]({{ $url }})

+ 9 - 0
resources/views/vendor/mail/text/layout.blade.php

@@ -0,0 +1,9 @@
+{!! strip_tags($header ?? '') !!}
+
+{!! strip_tags($slot) !!}
+@isset($subcopy)
+
+{!! strip_tags($subcopy) !!}
+@endisset
+
+{!! strip_tags($footer ?? '') !!}

+ 27 - 0
resources/views/vendor/mail/text/message.blade.php

@@ -0,0 +1,27 @@
+<x-mail::layout>
+    {{-- Header --}}
+    <x-slot:header>
+        <x-mail::header :url="config('app.url')">
+            {{ config('app.name') }}
+        </x-mail::header>
+    </x-slot:header>
+
+    {{-- Body --}}
+    {{ $slot }}
+
+    {{-- Subcopy --}}
+    @isset($subcopy)
+        <x-slot:subcopy>
+            <x-mail::subcopy>
+                {{ $subcopy }}
+            </x-mail::subcopy>
+        </x-slot:subcopy>
+    @endisset
+
+    {{-- Footer --}}
+    <x-slot:footer>
+        <x-mail::footer>
+            © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
+        </x-mail::footer>
+    </x-slot:footer>
+</x-mail::layout>

+ 1 - 0
resources/views/vendor/mail/text/panel.blade.php

@@ -0,0 +1 @@
+{{ $slot }}

+ 1 - 0
resources/views/vendor/mail/text/subcopy.blade.php

@@ -0,0 +1 @@
+{{ $slot }}

+ 1 - 0
resources/views/vendor/mail/text/table.blade.php

@@ -0,0 +1 @@
+{{ $slot }}