├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── inbox.php ├── database └── migrations │ ├── 2018_06_14_195930_create_threads_table.php │ ├── 2018_06_14_195931_create_messages_table.php │ └── 2018_06_14_195932_create_participants_table.php ├── resources ├── lang │ ├── ar │ │ ├── messages.php │ │ └── strings.php │ └── en │ │ ├── messages.php │ │ └── strings.php └── views │ ├── form.blade.php │ ├── index.blade.php │ ├── layouts │ ├── app.blade.php │ └── partials │ │ ├── navbar.blade.php │ │ └── sidebar.blade.php │ ├── loop │ ├── message.blade.php │ └── thread.blade.php │ └── show.blade.php ├── routes └── web.php └── src ├── EventMap.php ├── Events ├── NewMessageDispatched.php └── NewReplyDispatched.php ├── Http └── Controllers │ ├── Controller.php │ └── InboxController.php ├── InboxServiceProvider.php ├── Listeners └── SendNotification.php ├── Models ├── Message.php ├── Participant.php └── Thread.php ├── Notifications └── MessageDispatched.php └── Traits └── HasInbox.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 8.0.0 2 | 3 | * Laravel 8 support 4 | * 5 | ## 7.0.0 6 | 7 | * Laravel 7 support 8 | 9 | ## 5.0.0 10 | 11 | * Laravel 5 support 12 | 13 | ## 1.0.0 14 | 15 | * Beta version 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Mohamed Kawsara - Multicaret 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Inbox 2 | Internal messages between users in Laravel. 3 | Create an inbox system, in app messages, between users easily. 4 | 5 | [![Latest Version](https://img.shields.io/github/release/multicaret/laravel-inbox.svg?style=flat-square)](https://github.com/multicaret/laravel-inbox/releases) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/multicaret/laravel-inbox.svg?style=flat-square)](https://packagist.org/packages/multicaret/laravel-inbox) 7 | [![License](https://poser.pugx.org/multicaret/laravel-inbox/license.svg?style=flat-square)](https://packagist.org/packages/multicaret/laravel-inbox) 8 | 9 | 10 | 11 | ## Installation 12 | 13 | This package can be installed through Composer. 14 | 15 | ``` bash 16 | composer require multicaret/laravel-inbox 17 | ``` 18 | 19 | If you don't use Laravel 5.5+ you have to add the service provider manually 20 | 21 | ```php 22 | // config/app.php 23 | 'providers' => [ 24 | ... 25 | Multicaret\Inbox\InboxServiceProvider::class, 26 | ... 27 | ]; 28 | ``` 29 | 30 | You can publish the config-file with: 31 | 32 | ``` bash 33 | php artisan vendor:publish --provider="Multicaret\Inbox\InboxServiceProvider" --tag="config" 34 | ``` 35 | 36 | This is the contents of the published config file: 37 | 38 | ```php 39 | 10, 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Inbox Route Group Config 48 | |-------------------------------------------------------------------------- 49 | | 50 | | .. 51 | | 52 | */ 53 | 54 | 'route' => [ 55 | 'prefix' => 'inbox', 56 | 'middleware' => ['web', 'auth'], 57 | 'name' => null 58 | ], 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Inbox Tables Name 63 | |-------------------------------------------------------------------------- 64 | | 65 | | .. 66 | | 67 | */ 68 | 69 | 'tables' => [ 70 | 'threads' => 'threads', 71 | 'messages' => 'messages', 72 | 'participants' => 'participants', 73 | ], 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Models 78 | |-------------------------------------------------------------------------- 79 | | 80 | | If you want to overwrite any model you should change it here as well. 81 | | 82 | */ 83 | 84 | 'models' => [ 85 | 'thread' => Multicaret\Inbox\Models\Thread::class, 86 | 'message' => Multicaret\Inbox\Models\Message::class, 87 | 'participant' => Multicaret\Inbox\Models\Participant::class, 88 | ], 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Inbox Notification 93 | |-------------------------------------------------------------------------- 94 | | 95 | | Via Supported: "mail", "database", "array" 96 | | 97 | */ 98 | 99 | 'notifications' => [ 100 | 'via' => [ 101 | 'mail', 102 | ], 103 | ], 104 | ]; 105 | ``` 106 | 107 | ## Usage 108 | 109 | First, we need to use `HasInbox` trait so users can have their inbox: 110 | 111 | ```php 112 | threads() 131 | ``` 132 | 133 | #### Get unread messages: 134 | 135 | ```php 136 | $thread = $user->unread() 137 | ``` 138 | 139 | #### Get the threads that have been sent by a user: 140 | 141 | ```php 142 | $thread = $user->sent() 143 | ``` 144 | 145 | #### Get the threads that have been sent to the user: 146 | 147 | ```php 148 | $thread = $user->received() 149 | ``` 150 | 151 | #### Send new thread: 152 | 153 | - `subject()`: your message subject 154 | - `writes()`: your message body 155 | - `to()`: array of users ID that you want them to receive your message 156 | - `send()`: to send your message 157 | 158 | ```php 159 | $thread = $user->subject($request->subject) 160 | ->writes($request->body) 161 | ->to($request->recipients) 162 | ->send(); 163 | ``` 164 | 165 | #### Reply for thread: 166 | 167 | - `reply()` an object for your thread 168 | 169 | ```php 170 | $message = $user->writes($request->body) 171 | ->reply($thread); 172 | ``` 173 | 174 | #### Check if the thread has any unread messages: 175 | 176 | ```php 177 | if ($thread->isUnread()) 178 | ``` 179 | 180 | ## License 181 | 182 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 183 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multicaret/laravel-inbox", 3 | "description": "Laravel messages and inbox system", 4 | "type": "library", 5 | "keywords": [ 6 | "laravel", 7 | "inbox", 8 | "messages", 9 | "users", 10 | "messenger" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Mohamed Kawsara", 15 | "email": "mkwsra@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.2.5", 20 | "illuminate/support": "8.*", 21 | "nesbot/carbon": "2.*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Multicaret\\Inbox\\": "src/" 26 | } 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "Multicaret\\Inbox\\InboxServiceProvider" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/inbox.php: -------------------------------------------------------------------------------- 1 | 10, 6 | 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Inbox Route Group Config 10 | |-------------------------------------------------------------------------- 11 | | 12 | | 13 | */ 14 | 15 | 'route' => [ 16 | 'prefix' => 'inbox', 17 | 'middleware' => ['web', 'auth'], 18 | 'name' => null 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Inbox Tables Name 24 | |-------------------------------------------------------------------------- 25 | | 26 | | .. 27 | | 28 | */ 29 | 30 | 'tables' => [ 31 | 'threads' => 'threads', 32 | 'messages' => 'messages', 33 | 'participants' => 'participants', 34 | ], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Models 39 | |-------------------------------------------------------------------------- 40 | | 41 | | If you want to overwrite any model you should change it here as well. 42 | | 43 | */ 44 | 45 | 'models' => [ 46 | 'thread' => Multicaret\Inbox\Models\Thread::class, 47 | 'message' => Multicaret\Inbox\Models\Message::class, 48 | 'participant' => Multicaret\Inbox\Models\Participant::class, 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Inbox Notification 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Via Supported: "mail", "database", "array" 57 | | 58 | */ 59 | 60 | 'notifications' => [ 61 | 'via' => [ 62 | 'mail', 63 | ], 64 | ], 65 | ]; 66 | -------------------------------------------------------------------------------- /database/migrations/2018_06_14_195930_create_threads_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('user_id')->unsigned()->index(); 19 | $table->string('subject'); 20 | // $table->integer('status')->def; 21 | 22 | $table->softDeletes(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('threads'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2018_06_14_195931_create_messages_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('user_id')->unsigned()->index(); 19 | $table->integer('thread_id')->unsigned()->index(); 20 | $table->text('body'); 21 | 22 | $table->foreign('thread_id')->references('id')->on(config('inbox.tables.threads', 'threads'))->onDelete('cascade'); 23 | 24 | $table->softDeletes(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('messages'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2018_06_14_195932_create_participants_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('user_id')->unsigned()->index(); 19 | $table->integer('thread_id')->unsigned()->index(); 20 | $table->timestamp('seen_at')->nullable(); 21 | 22 | $table->foreign('thread_id') 23 | ->references('id') 24 | ->on(config('inbox.tables.threads', 'threads')) 25 | ->onDelete('cascade'); 26 | 27 | $table->softDeletes(); 28 | $table->timestamps(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('participants'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resources/lang/ar/messages.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'sent' => 'تم ارسال رسالتك بنجاح.', 16 | 'whoops' => 'يبدو انه هناك مشكلة ما!', 17 | 'deleted' => 'تم حذف الرسالة', 18 | ], 19 | 20 | 'message' => [ 21 | 'sent' => 'تم ارسال رسالتك بنجاح.', 22 | 'whoops' => 'يبدو انه هناك مشكلة ما!', 23 | ], 24 | 25 | 'notification' => [ 26 | 'subject' => 'ارسل رسالة جديدة', 27 | 'button' => 'افتح الرسالة' 28 | ], 29 | ]; 30 | -------------------------------------------------------------------------------- /resources/lang/ar/strings.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'to' => 'الى', 16 | 'subject' => 'الموضوع', 17 | 'body' => 'محتوى الرسالة', 18 | 'send' => 'ارسل', 19 | ], 20 | 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'sent' => 'Your message has been sent.', 15 | 'whoops' => 'Whoops something went wrong.', 16 | 'deleted' => 'Your message has been deleted', 17 | ], 18 | 19 | 'message' => [ 20 | 'sent' => 'Your message has been sent.', 21 | 'whoops' => 'Whoops something went wrong.', 22 | ], 23 | 24 | 'notification' => [ 25 | 'subject' => 'Sent a new message', 26 | 'button' => 'Open Message' 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /resources/lang/en/strings.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'to' => 'To', 16 | 'subject' => 'Subject', 17 | 'body' => 'Body', 18 | 'send' => 'Send', 19 | ], 20 | 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/views/form.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 | @if ($errors->has('recipients')) 16 | 17 | {{ $errors->first('recipients') }} 18 | 19 | @endif 20 |
21 | 22 |
23 | 24 | 25 | @if ($errors->has('subject')) 26 | 27 | {{ $errors->first('subject') }} 28 | 29 | @endif 30 |
31 | 32 |
33 | 34 | 35 | @if ($errors->has('body')) 36 | 37 | {{ $errors->first('body') }} 38 | 39 | @endif 40 |
-------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('inbox::layouts.app') 2 | @section('title', 'Messages') 3 | @section('content') 4 |
5 | @forelse($threads as $thread) 6 | @include('inbox::loop.thread') 7 | @empty 8 |
9 |

There are no messages

10 |
11 | @endforelse 12 |
13 | @endsection 14 | 15 | @section('pagination') 16 | @if($threads->hasPages()) 17 | {!! $threads->render() !!} 18 | @endif 19 | @endsection 20 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | Inbox 14 | 15 | 37 | 38 | 39 | @include('inbox::layouts.partials.navbar') 40 |
41 | 42 | {{-- 43 |
44 |
45 | 55 | 56 |
57 |
58 | 59 | 72 | 73 | 76 | 77 | 78 | 88 |
89 |
90 |
91 | --}} 92 |
93 |
94 | @include('inbox::layouts.partials.sidebar') 95 |
96 |
97 | 98 | 118 | 119 | 120 |
121 |
122 | @yield('content') 123 |
124 | 125 | {{-- 126 |
127 |
128 |
129 | This tab is empty. 130 |
131 |
132 |
133 |
134 | ... 135 |
136 | --}} 137 |
138 | 139 |
140 | @yield('pagination') 141 |
142 |
143 |
144 |
145 | 146 | 147 | 167 | 168 | @if(session()->has('message')) 169 |
171 | {{ session('message')['text'] }} 172 | 175 |
176 | @endif 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/navbar.blade.php: -------------------------------------------------------------------------------- 1 | 60 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/sidebar.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Compose 4 | 5 |
6 | 7 | 37 | -------------------------------------------------------------------------------- /resources/views/loop/message.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ optional($message->user)->name }} 4 |
5 |
{{ optional($message->user)->name }}
6 |
7 | {!! nl2br(e($message->body)) !!} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /resources/views/loop/thread.blade.php: -------------------------------------------------------------------------------- 1 | 3 |
4 | 7 |
8 | 9 | @if($thread->isUnread()) 10 | New 11 | @endif 12 | 13 | {{ $thread->user->username }} 14 | {{ $thread->messages->count() }} 15 | {{ $thread->subject }} 16 | - {{ str_limit($thread->lastMessage()->body, 50) }} 17 | {{ $thread->updated_at->diffForHumans() }} 18 | {{--
--}} 19 | {{----}} 20 | 21 |
23 | @csrf 24 | @method('delete') 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /resources/views/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('inbox::layouts.app') 2 | @section('title', 'Message: '. $thread->subject) 3 | @section('content') 4 |

{{ $thread->subject }}

5 | From: {{ $thread->user->name }} 6 | 7 | @if($thread->recipients) 8 | To: 9 | @foreach($thread->recipients as $recipient) 10 | {{ $recipient->name }} 11 | {{ $thread->recipients->last()->id != $recipient->id ? ', ' : '' }} 12 | @endforeach 13 | 14 | @endif 15 | 16 |
17 | 18 | @foreach($messages as $message) 19 | @include('inbox::loop.message') 20 | @endforeach 21 | 22 |
23 | @csrf 24 | 25 |
26 | 27 | 28 | @if ($errors->has('body')) 29 | 30 | {{ $errors->first('body') }} 31 | 32 | @endif 33 |
34 | 35 |
36 | 37 |
38 |
39 | @stop 40 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('inbox.index'); 6 | Route::get('create', 'InboxController@create')->name('inbox.create'); 7 | Route::post('store', 'InboxController@store')->name('inbox.store'); 8 | Route::post('{thread}/reply', 'InboxController@reply')->name('inbox.reply'); 9 | Route::get('{thread}', 'InboxController@show')->name('inbox.show'); 10 | Route::delete('{thread}/destroy', 'InboxController@destroy')->name('inbox.destroy'); -------------------------------------------------------------------------------- /src/EventMap.php: -------------------------------------------------------------------------------- 1 | [ 14 | Listeners\SendNotification::class, 15 | ], 16 | 17 | Events\NewReplyDispatched::class => [ 18 | Listeners\SendNotification::class, 19 | ], 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/NewMessageDispatched.php: -------------------------------------------------------------------------------- 1 | thread = $thread; 23 | $this->message = $message; 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Events/NewReplyDispatched.php: -------------------------------------------------------------------------------- 1 | thread = $thread; 23 | $this->message = $message; 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | composer('inbox::*', function ($view) { 17 | $view->with([ 18 | 'recipients' => config('auth.providers.users.model')::where('id', '!=', auth()->id())->get(), 19 | ]); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/InboxController.php: -------------------------------------------------------------------------------- 1 | threadClass = config('inbox.models.thread'); 19 | $this->participantClass = config('inbox.models.participant'); 20 | } 21 | 22 | /** 23 | * Display a listing of the resource. 24 | * 25 | * @return \Illuminate\Http\Response 26 | */ 27 | public function index() 28 | { 29 | $user = auth()->user(); 30 | if (request()->has('sent')) { 31 | $threads = $user->sent(); 32 | } else { 33 | $threads = $user->received(); 34 | } 35 | 36 | $threads = $threads->paginate(config('inbox.paginate', 10)); 37 | 38 | 39 | return view('inbox::index', compact('threads')); 40 | } 41 | 42 | /** 43 | * 44 | */ 45 | public function create() 46 | { 47 | return view('inbox::create'); 48 | } 49 | 50 | /** 51 | * Store a newly created resource in storage. 52 | * 53 | * @param \Illuminate\Http\Request $request 54 | * 55 | * @return \Illuminate\Http\Response 56 | */ 57 | public function store(Request $request) 58 | { 59 | $request->validate([ 60 | 'subject' => 'required', 61 | 'body' => 'required', 62 | 'recipients' => 'required|array', 63 | ]); 64 | 65 | $thread = auth()->user() 66 | ->subject($request->subject) 67 | ->writes($request->body) 68 | ->to($request->recipients) 69 | ->send(); 70 | 71 | return redirect() 72 | ->route(config('inbox.route.name') . 'inbox.index') 73 | ->with('message', [ 74 | 'type' => $thread ? 'success' : 'error', 75 | 'text' => $thread ? trans('inbox::messages.thread.sent') : trans('inbox::messages.thread.whoops'), 76 | ]); 77 | } 78 | 79 | /** 80 | * Display the specified resource. 81 | * 82 | * @param Thread $thread 83 | * 84 | * @return \Illuminate\Http\Response 85 | */ 86 | public function show($thread) 87 | { 88 | $threadClass = $this->threadClass; 89 | $thread = $threadClass::findOrFail($thread); 90 | 91 | $messages = $thread->messages()->get(); 92 | 93 | $seen = $thread->participants() 94 | ->where('user_id', auth()->id()) 95 | ->first(); 96 | 97 | if ($seen && $seen->pivot) { 98 | $seen->pivot->seen_at = Carbon::now(); 99 | $seen->pivot->save(); 100 | } else { 101 | return abort(404); 102 | } 103 | 104 | return view('inbox::show', compact('messages', 'thread')); 105 | } 106 | 107 | /** 108 | * Store a newly created resource in storage. 109 | * 110 | * @param \Illuminate\Http\Request $request 111 | * @param Thread $thread 112 | * 113 | * @return \Illuminate\Http\Response 114 | */ 115 | public function reply(Request $request, $thread) 116 | { 117 | $threadClass = $this->threadClass; 118 | $thread = $threadClass::findOrFail($thread); 119 | 120 | $request->validate([ 121 | 'body' => 'required', 122 | ]); 123 | 124 | $message = auth()->user() 125 | ->writes($request->body) 126 | ->reply($thread); 127 | 128 | return redirect() 129 | ->route(config('inbox.route.name') . 'inbox.show', $thread) 130 | ->with('message', [ 131 | 'type' => $message ? 'success' : 'error', 132 | 'text' => $message ? trans('inbox::messages.message.sent') : trans('inbox::messages.message.whoops'), 133 | ]); 134 | } 135 | 136 | /** 137 | * Remove the specified resource from storage. 138 | * 139 | * @param Thread $thread 140 | * 141 | * @return \Illuminate\Http\Response 142 | */ 143 | public function destroy($thread) 144 | { 145 | $threadClass = $this->threadClass; 146 | $thread = $threadClass::findOrFail($thread); 147 | 148 | $message = Participant::where('user_id', auth()->id()) 149 | ->where('thread_id', $thread->id) 150 | ->firstOrFail(); 151 | 152 | $deleted = $message->delete(); 153 | 154 | return redirect() 155 | ->route(config('inbox.route.name') . 'inbox.index') 156 | ->with('message', [ 157 | 'type' => $deleted ? 'success' : 'error', 158 | 'text' => $deleted ? trans('inbox::messages.thread.deleted') : trans('inbox::messages.thread.whoops'), 159 | ]); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/InboxServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerEvents(); 21 | $this->registerMigrations(); 22 | $this->registerRoutes(); 23 | $this->registerResources(); 24 | $this->registerTranslations(); 25 | } 26 | 27 | /** 28 | * Register the Inbox events. 29 | * 30 | * @return void 31 | */ 32 | protected function registerEvents() 33 | { 34 | $events = $this->app->make(Dispatcher::class); 35 | 36 | foreach ($this->events as $event => $listeners) { 37 | foreach ($listeners as $listener) { 38 | $events->listen($event, $listener); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Register the Inbox routes. 45 | * 46 | * @return void 47 | */ 48 | protected function registerRoutes() 49 | { 50 | Route::group([ 51 | 'prefix' => config('inbox.route.prefix', 'inbox'), 52 | 'namespace' => 'Multicaret\Inbox\Http\Controllers', 53 | 'middleware' => config('inbox.route.middleware', ['web', 'auth']), 54 | 'as' => config('inbox.route.name') 55 | ], function () { 56 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 57 | }); 58 | } 59 | 60 | /** 61 | * Register the Inbox resources. 62 | * 63 | * @return void 64 | */ 65 | protected function registerResources() 66 | { 67 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'inbox'); 68 | } 69 | 70 | /** 71 | * Register Inbox's migration files. 72 | * 73 | * @return void 74 | */ 75 | protected function registerMigrations() 76 | { 77 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 78 | } 79 | 80 | /** 81 | * Register Inbox's translations files. 82 | * 83 | * @return void 84 | */ 85 | protected function registerTranslations() 86 | { 87 | $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'inbox'); 88 | } 89 | 90 | /** 91 | * Register any application services. 92 | * 93 | * @return void 94 | */ 95 | public function register() 96 | { 97 | $this->configure(); 98 | $this->offerPublishing(); 99 | } 100 | 101 | /** 102 | * Setup the configuration for Inbox. 103 | * 104 | * @return void 105 | */ 106 | protected function configure() 107 | { 108 | $this->mergeConfigFrom( 109 | __DIR__.'/../config/inbox.php', 'inbox' 110 | ); 111 | } 112 | 113 | /** 114 | * Setup the resource publishing groups for Inbox. 115 | * 116 | * @return void 117 | */ 118 | protected function offerPublishing() 119 | { 120 | if ($this->app->runningInConsole()) { 121 | $this->publishes([ 122 | __DIR__.'/../config/inbox.php' => config_path('inbox.php'), 123 | ], 'inbox-config'); 124 | 125 | // $this->publishes([ 126 | // __DIR__ . '/../database/migrations' => database_path('migrations'), 127 | // ], 'inbox-migrations'); 128 | 129 | $this->publishes([ 130 | __DIR__.'/../resources/views' => resource_path('views/vendor/inbox'), 131 | ], 'inbox-views'); 132 | 133 | $this->publishes([ 134 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/inbox'), 135 | ], 'inbox-translations'); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Listeners/SendNotification.php: -------------------------------------------------------------------------------- 1 | thread; 19 | $message = $event->message; 20 | 21 | $participants = $thread->participants() 22 | ->where('user_id', '!=', $message->user_id) 23 | ->get(); 24 | 25 | if ($participants->count()) { 26 | foreach ($participants as $participant) { 27 | $participant->notify(new MessageDispatched($thread, $message, $participant)); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Message.php: -------------------------------------------------------------------------------- 1 | table = config('inbox.tables.messages'); 25 | 26 | parent::__construct($attributes); 27 | } 28 | 29 | /** 30 | * The attributes that can be set with Mass Assignment. 31 | * 32 | * @var array 33 | */ 34 | protected $fillable = ['thread_id', 'user_id', 'body']; 35 | 36 | /** 37 | * Thread relationship 38 | * 39 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 40 | */ 41 | public function thread() 42 | { 43 | return $this->belongsTo(config('inbox.models.thread')); 44 | } 45 | 46 | /** 47 | * Participants relationship 48 | * 49 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 50 | */ 51 | public function participants() 52 | { 53 | return $this->hasMany(config('inbox.models.participant'), 'thread_id', 'thread_id'); 54 | } 55 | 56 | /** 57 | * User relationship 58 | * 59 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 60 | */ 61 | public function user() 62 | { 63 | return $this->belongsTo(config('auth.providers.users.model')); 64 | } 65 | 66 | /** 67 | * Set the user id. 68 | * 69 | * @param string $value 70 | * 71 | * @return void 72 | */ 73 | public function setUserIdAttribute($value) 74 | { 75 | $this->attributes['user_id'] = (int)$value; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Models/Participant.php: -------------------------------------------------------------------------------- 1 | table = config('inbox.tables.participants'); 22 | 23 | parent::__construct($attributes); 24 | } 25 | 26 | /** 27 | * The attributes that can be set with Mass Assignment. 28 | * 29 | * @var array 30 | */ 31 | protected $fillable = ['thread_id', 'user_id', 'seen_at']; 32 | 33 | /** 34 | * The attributes that should be mutated to dates. 35 | * 36 | * @var array 37 | */ 38 | // protected $dates = ['created_at', 'updated_at', 'deleted_at', 'seen_at']; 39 | 40 | /** 41 | * Thread relationship 42 | * 43 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 44 | */ 45 | public function thread() 46 | { 47 | return $this->belongsTo(config('inbox.models.thread')); 48 | } 49 | 50 | /** 51 | * User relationship 52 | * 53 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 54 | */ 55 | public function user() 56 | { 57 | return $this->belongsTo(config('auth.providers.users.model')); 58 | } 59 | 60 | /** 61 | * Returns threads with new messages that the user is associated with 62 | * 63 | * @param $query 64 | * @param $userId 65 | * 66 | * @return mixed 67 | */ 68 | public function scopeInbox($query, $userId) 69 | { 70 | return $query->where('user_id', $userId); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Models/Thread.php: -------------------------------------------------------------------------------- 1 | table = config('inbox.tables.threads'); 25 | 26 | parent::__construct($attributes); 27 | } 28 | 29 | /** 30 | * The attributes that can be set with Mass Assignment. 31 | * 32 | * @var array 33 | */ 34 | protected $fillable = ['subject', 'user_id']; 35 | 36 | /** 37 | * Messages relationship 38 | * 39 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 40 | */ 41 | public function messages() 42 | { 43 | return $this->hasMany(config('inbox.models.message')); 44 | } 45 | 46 | /** 47 | * Participants relationship 48 | * 49 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 50 | */ 51 | public function participants() 52 | { 53 | return $this->belongsToMany(config('auth.providers.users.model'), config('inbox.tables.participants'), 54 | 'thread_id', 'user_id') 55 | ->withPivot('seen_at', 'deleted_at') 56 | ->withTimestamps(); 57 | } 58 | 59 | /** 60 | * Recipients of this message 61 | * 62 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 63 | */ 64 | public function recipients() 65 | { 66 | return $this->participants()->where('user_id', '!=', $this->user_id); 67 | } 68 | 69 | /** 70 | * User relationship 71 | * 72 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 73 | */ 74 | public function user() 75 | { 76 | return $this->belongsTo(config('auth.providers.users.model')); 77 | } 78 | 79 | /** 80 | * See if the current thread is unread by the user. 81 | * 82 | * @param integer $userId 83 | * 84 | * @return bool 85 | */ 86 | public function isUnread($userId = null) 87 | { 88 | $userId = $userId ?? auth()->id(); 89 | 90 | $participant = $this->participants() 91 | ->where('user_id', $userId) 92 | ->first(); 93 | 94 | if ($participant && $participant->pivot->seen_at === null) { 95 | return true; 96 | } 97 | 98 | return false; 99 | } 100 | 101 | /** 102 | * Adds users to this thread 103 | * 104 | * @param array $participants list of all participants 105 | * 106 | * @return void 107 | */ 108 | public function addParticipants(array $participants) 109 | { 110 | if (count($participants)) { 111 | $participantClass = config('inbox.models.participant'); 112 | foreach ($participants as $user_id) { 113 | $participant = $participantClass::firstOrCreate([ 114 | 'thread_id' => $this->id, 115 | 'user_id' => $user_id, 116 | ]); 117 | 118 | $participant->seen_at = null; 119 | $participant->save(); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * Restores all participants within a thread that has a new message 126 | */ 127 | public function activateAllParticipants() 128 | { 129 | $participants = $this->participants()->get(); 130 | 131 | foreach ($participants as $participant) { 132 | $participant = $participant->pivot; 133 | if ($participant) { 134 | $participant->deleted_at = null; 135 | $participant->seen_at = null; 136 | $participant->save(); 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Get last message associated with thread. 143 | * 144 | * @return object 145 | */ 146 | public function lastMessage() 147 | { 148 | return $this->messages()->latest()->first(); 149 | } 150 | 151 | /** 152 | * Scope a query to only include thread form custom users. 153 | * 154 | * @param \Illuminate\Database\Eloquent\Builder $query 155 | * @param $users 156 | * 157 | * @return void 158 | */ 159 | public function scopeFrom($query, $users) 160 | { 161 | if ( ! is_array($users)) { 162 | $users = [$users]; 163 | } 164 | 165 | $query->whereIn($this->table . '.user_id', $users); 166 | } 167 | 168 | /** 169 | * Scope a query to only include thread sent to custom users. 170 | * 171 | * @param \Illuminate\Database\Eloquent\Builder $query 172 | * @param $users 173 | * 174 | * @return void 175 | */ 176 | public function scopeTo($query, $users) 177 | { 178 | if ( ! is_array($users)) { 179 | $users = [$users]; 180 | } 181 | 182 | $participantsTable = config('inbox.tables.participants'); 183 | 184 | $query->select($this->table . '.*') 185 | ->join($participantsTable, "{$this->table}.id", '=', "{$participantsTable}.thread_id") 186 | ->whereIn("{$participantsTable}.user_id", $users); 187 | } 188 | 189 | /** 190 | * Scope a query to only include seen thread. 191 | * 192 | * @param \Illuminate\Database\Eloquent\Builder $query 193 | * 194 | * @return void 195 | */ 196 | public function scopeSeen($query) 197 | { 198 | $query->whereNotNull(config('inbox.tables.participants') . '.seen_at'); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Notifications/MessageDispatched.php: -------------------------------------------------------------------------------- 1 | thread = $thread; 27 | $this->message = $message; 28 | $this->participant = $participant; 29 | } 30 | 31 | /** 32 | * Get the notification's delivery channels. 33 | * 34 | * @param mixed $notifiable 35 | * 36 | * @return array 37 | */ 38 | public function via($notifiable) 39 | { 40 | return config('inbox.notifications.via', [ 41 | 'mail' 42 | ]); 43 | } 44 | 45 | /** 46 | * Get the array representation of the notification. 47 | * 48 | * @param mixed $notifiable 49 | * 50 | * @return array 51 | */ 52 | public function toArray($notifiable) 53 | { 54 | return [ 55 | 'thread_id' => $this->thread->id, 56 | 'message_id' => $this->message->id, 57 | 'isReply' => $this->thread->messages()->count() >= 2, 58 | ]; 59 | } 60 | 61 | /** 62 | * Get the database representation of the notification. 63 | * 64 | * @param mixed $notifiable 65 | * 66 | * @return array 67 | */ 68 | public function toDatabase($notifiable) 69 | { 70 | return [ 71 | 'thread_id' => $this->thread->id, 72 | 'message_id' => $this->message->id, 73 | 'isReply' => $this->thread->messages()->count() >= 2, 74 | ]; 75 | } 76 | 77 | /** 78 | * Get the mail representation of the notification. 79 | * 80 | * @param mixed $notifiable 81 | * 82 | * @return \Illuminate\Notifications\Messages\MailMessage 83 | * @throws \Throwable 84 | */ 85 | public function toMail($notifiable) 86 | { 87 | $buttonUrl = route(config('inbox.route.name') . 'inbox.show', $this->thread); 88 | $isReply = $this->thread->messages()->count() >= 2; 89 | $greeting = $isReply ? 'Re: ' . $this->thread->subject : $this->thread->subject; 90 | 91 | return (new MailMessage) 92 | ->success() 93 | ->subject($this->message->user->name . ' ' . trans('inbox::messages.notification.subject') . ' - ' . config('app.name')) 94 | ->greeting($greeting) 95 | ->line($this->message->body) 96 | ->action(trans('inbox::messages.notification.button'), $buttonUrl); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Traits/HasInbox.php: -------------------------------------------------------------------------------- 1 | threadsTable = config('inbox.tables.threads'); 27 | $this->messagesTable = config('inbox.tables.messages'); 28 | $this->participantsTable = config('inbox.tables.participants'); 29 | 30 | $this->threadClass = config('inbox.models.thread'); 31 | $this->participantClass = config('inbox.models.participant'); 32 | 33 | parent::__construct($attributes); 34 | } 35 | 36 | public function subject($subject) 37 | { 38 | $this->subject = $subject; 39 | 40 | return $this; 41 | } 42 | 43 | public function writes($message) 44 | { 45 | $this->message = $message; 46 | 47 | return $this; 48 | } 49 | 50 | public function to($users) 51 | { 52 | if (is_array($users)) { 53 | $this->recipients = array_merge($this->recipients, $users); 54 | } else { 55 | $this->recipients[] = $users; 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | 62 | /** 63 | * Send new message 64 | * 65 | * @return mixed 66 | */ 67 | public function send() 68 | { 69 | $thread = $this->threads()->create([ 70 | 'subject' => $this->subject, 71 | ]); 72 | 73 | // Message 74 | $message = $thread->messages()->create([ 75 | 'user_id' => $this->id, 76 | 'body' => $this->message 77 | ]); 78 | 79 | // Sender 80 | $participantClass = $this->participantClass; 81 | $participantClass::create([ 82 | 'user_id' => $this->id, 83 | 'thread_id' => $thread->id, 84 | 'seen_at' => Carbon::now() 85 | ]); 86 | 87 | if (count($this->recipients)) { 88 | $thread->addParticipants($this->recipients); 89 | } 90 | 91 | if ($thread) { 92 | event(new NewMessageDispatched($thread, $message)); 93 | } 94 | 95 | return $thread; 96 | } 97 | 98 | /** 99 | * Store a newly created resource in storage. 100 | * 101 | * @param Thread $thread 102 | * 103 | * @return \Illuminate\Http\Response 104 | */ 105 | public function reply($thread) 106 | { 107 | if ( ! is_object($thread)) { 108 | $threadClass = $this->threadClass; 109 | $thread = $threadClass::whereId($thread)->firstOrFail(); 110 | } 111 | 112 | $thread->activateAllParticipants(); 113 | 114 | $message = $thread->messages()->create([ 115 | 'user_id' => $this->id, 116 | 'body' => $this->message 117 | ]); 118 | 119 | // Add replier as a participant 120 | $participantClass = $this->participantClass; 121 | $participant = $participantClass::firstOrCreate([ 122 | 'thread_id' => $thread->id, 123 | 'user_id' => $this->id 124 | ]); 125 | 126 | $participant->seen_at = Carbon::now(); 127 | $participant->save(); 128 | 129 | $thread->updated_at = Carbon::now(); 130 | $thread->save(); 131 | 132 | event(new NewReplyDispatched($thread, $message)); 133 | 134 | return $message; 135 | } 136 | 137 | /** 138 | * Get user threads 139 | * 140 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 141 | */ 142 | public function threads() 143 | { 144 | return $this->hasMany($this->threadClass); 145 | } 146 | 147 | /** 148 | * 149 | * @param bool $withTrashed 150 | * 151 | * @return \Illuminate\Database\Eloquent\Relations\belongsToMany 152 | */ 153 | public function participated($withTrashed = false) 154 | { 155 | $query = $this->belongsToMany($this->threadClass, $this->participantsTable, 'user_id', 'thread_id') 156 | ->withPivot('seen_at') 157 | ->withTimestamps(); 158 | 159 | if ( ! $withTrashed) { 160 | $query->whereNull("{$this->participantsTable}.deleted_at"); 161 | } 162 | 163 | return $query; 164 | } 165 | 166 | /** 167 | * Get the threads that have been sent to the user. 168 | * 169 | * @return \Illuminate\Database\Eloquent\Builder 170 | */ 171 | public function received() 172 | { 173 | // todo: get only the received messages if they got an answer 174 | return $this->participated()->latest('updated_at'); 175 | } 176 | 177 | /** 178 | * Get the threads that have been sent by a user. 179 | * 180 | * @return \Illuminate\Database\Eloquent\Builder 181 | */ 182 | public function sent() 183 | { 184 | return $this->participated() 185 | ->where("{$this->threadsTable}.user_id", $this->id) 186 | ->latest('updated_at'); 187 | } 188 | 189 | /** 190 | * Get unread messages 191 | * 192 | * @return \Illuminate\Database\Eloquent\Builder 193 | */ 194 | public function unread() 195 | { 196 | return $this->received()->whereNull('seen_at'); 197 | } 198 | } 199 | --------------------------------------------------------------------------------