├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── composer.json └── src ├── Facades └── Messenger.php ├── Http └── Controllers │ └── MessageController.php ├── LaravelMessengerServiceProvider.php ├── Messenger.php ├── Models ├── Conversation.php └── Message.php ├── assets ├── css │ └── messenger.css ├── js │ └── messenger-chat.js └── sounds │ └── tweet.mp3 ├── config └── messenger.php ├── database └── migrations │ ├── 2017_10_24_140229_create_conversaions_table.php │ └── 2017_10_24_140250_create_messages_table.php ├── routes └── messenger.php └── views ├── messenger.blade.php └── partials ├── messages.blade.php └── threads.blade.php /.gitignore: -------------------------------------------------------------------------------- 1 | .tags 2 | /vendor/ 3 | NOTES.PHP 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bakly Systems 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Chat Messenger 2 | A Laravel JQuery chat that is just like Facebook chat 3 | 4 | ## Installation 5 | 6 | Laravel Messenger supports Laravel 5.3 and higher. 7 | 8 | ### Package 9 | 10 | Require via composer 11 | 12 | ```bash 13 | $ composer require baklysystems/laravel-chat-messenger 14 | ``` 15 | 16 | In `config/app.php` file 17 | 18 | ```php 19 | 'providers' => [ 20 | ... 21 | BaklySystems\LaravelMessenger\LaravelMessengerServiceProvider::class, 22 | ... 23 | ]; 24 | 25 | 'aliases' => [ 26 | ... 27 | 'Messenger' => BaklySystems\LaravelMessenger\Facades\Messenger::class, 28 | ... 29 | ]; 30 | ``` 31 | 32 | ### Laravel Messenger Files 33 | 34 | Then, run `php artisan vendor:publish` to publish the config file, MessageController and assets and routes. 35 | ### Laravel Messenger Styles and Scripts 36 | 37 | Make sure to add `@yield('css-styles')` in your app/master head section and `@yield('js-scripts')` to your app/master scripts section, or edit section naming in `view/vendor/messenger/messenger.blade.php` 38 | 39 | JQuery is required for the messenger script. 40 | 41 | Make sure to add `include('messenger.php');` to your web.php 42 | 43 | ### Laravel Messenger Pusher 44 | 45 | Add your pusher keys in `config/messenger.php` file. 46 | 47 | And voila, you can start conversation with any user by linking to `your-domain.com/messenger/t/{userId}`. 48 | 49 | ## Customization 50 | 51 | ### Migrations 52 | 53 | To publish and edit messenger migrations, run the publish command with `messenger-migrations` tag. 54 | 55 | ```bash 56 | $ php artisan vendor:publish --tag messenger-migrations 57 | ``` 58 | ### Views 59 | 60 | To publish and edit messenger views, run the publish command with `messenger-views` tag. 61 | 62 | ```bash 63 | $ php artisan vendor:publish --tag messenger-views 64 | ``` 65 | 66 | ## TODO 67 | 68 | * emotions. 69 | * upload photos. 70 | * Attach files. 71 | * Show date before every conversation beginning. 72 | * paginate and load threads. 73 | * Laravel Messenger chatbox. 74 | * Unauthenticated chatbox to message customer service. 75 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | . threads doesn't reload page, use AJAX requests. 4 | . reorganize routes. 5 | . handle delete message fail. 6 | . handle load messages fail. 7 | . handle load threads fail. 8 | . handle make seen fail. 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baklysystems/laravel-chat-messenger", 3 | "description": "Laravel chat package", 4 | "license": "MIT", 5 | "keywords": [ 6 | "message", 7 | "inbox", 8 | "conversations", 9 | "chat", 10 | "php", 11 | "laravel", 12 | "realtime", 13 | "real-time", 14 | "talk", 15 | "fb chat", 16 | "like facebook chat", 17 | "like facebook messenger", 18 | "support chat" 19 | ], 20 | "homepage": "https://github.com/baklysystems/laravel-chat-messenger", 21 | "authors": [ 22 | { 23 | "name": "Mohamed Abdul-Fattah", 24 | "email": "csmohamed8@gmail.com" 25 | } 26 | ], 27 | "require": { 28 | "laravel/framework": ">=5.3.0", 29 | "illuminate/support": ">=5.3.0", 30 | "laravelcollective/html": ">=5.3.0", 31 | "pusher/pusher-php-server": ">=2.6" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "BaklySystems\\LaravelMessenger\\": "src/" 36 | } 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "BaklySystems\\LaravelMessenger\\LaravelMessengerServiceProvider" 42 | ], 43 | "aliases": { 44 | "Messenger": "BaklySystems\\LaravelMessenger\\Facades\\Messenger" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Facades/Messenger.php: -------------------------------------------------------------------------------- 1 | middleware(['web', 'auth']); 21 | } 22 | 23 | /** 24 | * Get messenger page. 25 | * 26 | * @param int $withId 27 | * @return Response 28 | */ 29 | public function laravelMessenger($withId) 30 | { 31 | Messenger::makeSeen(auth()->id(), $withId); 32 | $withUser = config('messenger.user.model', 'App\User')::findOrFail($withId); 33 | $messages = Messenger::messagesWith(auth()->id(), $withUser->id); 34 | $threads = Messenger::threads(auth()->id()); 35 | 36 | return view('messenger::messenger', compact('withUser', 'messages', 'threads')); 37 | } 38 | 39 | /** 40 | * Create a new message. 41 | * 42 | * @param \Illuminate\Http\Request $request 43 | * @return Response 44 | */ 45 | public function store(Request $request) 46 | { 47 | $this->validate($request, Message::rules()); 48 | 49 | $authId = auth()->id(); 50 | $withId = $request->withId; 51 | $conversation = Messenger::getConversation($authId, $withId); 52 | 53 | if (! $conversation) { 54 | $conversation = Messenger::newConversation($authId, $withId); 55 | } 56 | 57 | $message = Messenger::newMessage($conversation->id, $authId, $request->message); 58 | 59 | // Pusher 60 | $pusher = new Pusher( 61 | config('messenger.pusher.app_key'), 62 | config('messenger.pusher.app_secret'), 63 | config('messenger.pusher.app_id'), 64 | [ 65 | 'cluster' => config('messenger.pusher.options.cluster') 66 | ] 67 | ); 68 | $pusher->trigger('messenger-channel', 'messenger-event', [ 69 | 'message' => $message, 70 | 'senderId' => $authId, 71 | 'withId' => $withId 72 | ]); 73 | 74 | return response()->json([ 75 | 'success' => true, 76 | 'message' => $message 77 | ], 200); 78 | } 79 | 80 | /** 81 | * Load threads view. 82 | * 83 | * @param \Illuminate\Http\Request $request 84 | * @return Response. 85 | */ 86 | public function loadThreads(Request $request) 87 | { 88 | if ($request->ajax()) { 89 | $withUser = config('messenger.user.model', 'App\User')::findOrFail($request->withId); 90 | $threads = Messenger::threads(auth()->id()); 91 | $view = view('messenger::partials.threads', compact('threads', 'withUser'))->render(); 92 | 93 | return response()->json($view, 200); 94 | } 95 | } 96 | 97 | /** 98 | * Load more messages. 99 | * 100 | * @param \Illuminate\Http\Request $request 101 | * @return Response. 102 | */ 103 | public function moreMessages(Request $request) 104 | { 105 | $this->validate($request, ['withId' => 'required|integer']); 106 | 107 | if ($request->ajax()) { 108 | $messages = Messenger::messagesWith( 109 | auth()->id(), 110 | $request->withId, 111 | $request->take 112 | ); 113 | $view = view('messenger::partials.messages', compact('messages'))->render(); 114 | 115 | return response()->json([ 116 | 'view' => $view, 117 | 'messagesCount' => $messages->count() 118 | ], 200); 119 | } 120 | } 121 | 122 | /** 123 | * Make a message seen. 124 | * 125 | * @param \Illuminate\Http\Request $request 126 | * @return Response 127 | */ 128 | public function makeSeen(Request $request) 129 | { 130 | Messenger::makeSeen($request->authId, $request->withId); 131 | 132 | return response()->json(['success' => true], 200); 133 | } 134 | 135 | /** 136 | * Delete a message. 137 | * 138 | * @param int $id 139 | * @return Response. 140 | */ 141 | public function destroy($id) 142 | { 143 | $confirm = Messenger::deleteMessage($id, auth()->id()); 144 | 145 | return response()->json(['success' => true], 200); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/LaravelMessengerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | // config file. 18 | __DIR__.'/config/messenger.php' => config_path('messenger.php'), 19 | // controller. 20 | __DIR__.'/Http/Controllers/MessageController.php' 21 | => app_path('Http/Controllers/MessageController.php'), 22 | // assets. 23 | __DIR__.'/assets' => public_path('vendor/messenger'), 24 | // routes 25 | __DIR__.'/routes/' => base_path('/routes/'), 26 | ]); 27 | 28 | // routes. 29 | // $this->loadRoutesFrom(base_path('routes/messenger.php')); 30 | 31 | // migrations. 32 | $this->loadMigrationsFrom(__DIR__.'/database/migrations'); 33 | // publish under messenger-migrations tag. 34 | $this->publishes([ 35 | __DIR__.'/database/migrations' => database_path('migrations'), 36 | ], 'messenger-migrations'); 37 | 38 | // views. 39 | $this->loadViewsFrom(__DIR__.'/views', 'messenger'); 40 | // publish under messenger-views tag. 41 | $this->publishes([ 42 | __DIR__.'/views' => resource_path('views/vendor/messenger'), 43 | ], 'messenger-views'); 44 | } 45 | 46 | /** 47 | * Register the application services. 48 | * 49 | * @return void 50 | */ 51 | public function register() 52 | { 53 | // Messenger Facede. 54 | $this->app->singleton('messenger', function () { 55 | return new Messenger; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Messenger.php: -------------------------------------------------------------------------------- 1 | whereUserOne($authId) 34 | ->whereUserTwo($withId); 35 | })->orWhere(function ($query) use ($authId, $withId) { 36 | $query->whereUserOne($withId) 37 | ->whereUserTwo($authId); 38 | })->first(); 39 | 40 | return $conversation; 41 | } 42 | 43 | /** 44 | * Make a new conversation between two users. 45 | * 46 | * @param int $authId 47 | * @param int $withId 48 | * @return collection 49 | */ 50 | public function newConversation($authId, $withId) 51 | { 52 | $conversation = Conversation::create([ 53 | 'user_one' => $authId, 54 | 'user_two' => $withId 55 | ]); 56 | 57 | return $conversation; 58 | } 59 | 60 | /** 61 | * Get last {$take} conversations with all users for a user. 62 | * 63 | * @param int $authId 64 | * @param int $take (optional) 65 | * @return collection 66 | */ 67 | public function userConversations($authId, $take = 20) 68 | { 69 | $collection = Conversation::whereUserOne($authId) 70 | ->orWhere('user_two', $authId); 71 | $totalRecords = $collection->count(); 72 | $conversations = $collection->take($take) 73 | ->skip($totalRecords - $take) 74 | ->get(); 75 | 76 | return $conversations; 77 | } 78 | 79 | /** 80 | * Create a new message between two users. 81 | * 82 | * @param int $conversationId 83 | * @param int $senderId 84 | * @param string $message 85 | * @param boolean $isSeen (optional) 86 | * @param boolean $deletedSender (optional) 87 | * @param boolean $deletedReceiver (optional) 88 | * @return collection 89 | */ 90 | public function newMessage( 91 | $conversationId, 92 | $senderId, 93 | $message, 94 | $isSeen = 0, 95 | $deletedSender = 0, 96 | $deletedReceiver = 0 97 | ) 98 | { 99 | $message = Message::create([ 100 | 'conversation_id' => $conversationId, 101 | 'sender_id' => $senderId, 102 | 'message' => $message, 103 | 'is_seen' => $isSeen, 104 | 'deleted_from_sender' => $deletedSender, 105 | 'deleted_from_receiver' => $deletedReceiver 106 | ]); 107 | 108 | return $message; 109 | } 110 | 111 | /** 112 | * Get last {$take} messages between two users. 113 | * 114 | * @param int $authId 115 | * @param int $withId 116 | * @param int $take (optional) 117 | * @return collection 118 | */ 119 | public function messagesWith($authId, $withId, $take = 20) 120 | { 121 | $conversation = $this->getConversation($authId, $withId); 122 | 123 | if ($conversation) { 124 | $messages = Message::whereConversationId($conversation->id) 125 | ->where(function ($query) use ($authId, $withId) { 126 | $query->where(function ($qr) use ($authId) { 127 | $qr->where('sender_id', $authId) // this message is sent by the authUser. 128 | ->where('deleted_from_sender', 0); 129 | })->orWhere(function ($qr) use ($withId) { 130 | $qr->where('sender_id', $withId) // this message is sent by the receiver/withUser. 131 | ->where('deleted_from_receiver', 0); 132 | }); 133 | }) 134 | ->latest() 135 | ->take($take) 136 | ->get(); 137 | 138 | return $messages->reverse(); 139 | } 140 | 141 | return collect(); 142 | } 143 | 144 | /** 145 | * Get last {$take} user threads with all other users. 146 | * 147 | * @param int $authId 148 | * @param int $take (optional) 149 | * @return collection 150 | */ 151 | public function threads($authId, $take = 20) 152 | { 153 | $conversations = $this->userConversations($authId, $take); 154 | $threads = []; 155 | 156 | foreach ($conversations as $key => $conversation) { 157 | if ($conversation->user_one === $authId) { 158 | $withUser = $conversation->userTwo; 159 | } else { 160 | $withUser = $conversation->userOne; 161 | } 162 | $collection = (object) null; 163 | $collection->conversationId = $conversation->id; 164 | $collection->withUser = $withUser; 165 | $collection->lastMessage = $conversation->lastMessage(); 166 | $threads[] = $collection; 167 | } 168 | 169 | $threads = collect($threads); 170 | $threads = $threads->sortByDesc(function ($ins, $key) { // order threads by last updated message. 171 | $ins = (array) $ins; 172 | return $ins['lastMessage']['created_at']; 173 | }); 174 | 175 | return $threads->values()->all(); 176 | } 177 | 178 | /** 179 | * Make messages seen for a conversation. 180 | * 181 | * @param int $authId 182 | * @param int $withId 183 | * @return boolean 184 | */ 185 | public function makeSeen($authId, $withId) 186 | { 187 | $conversation = $this->getConversation($authId, $withId); 188 | if ($conversation) { 189 | Message::whereConversationId($conversation->id)->update([ 190 | 'is_seen' => 1 191 | ]); 192 | } 193 | 194 | return response()->json(['success' => true], 200); 195 | } 196 | 197 | /** 198 | * Delete a message. 199 | * 200 | * @param int $messageId 201 | * @param int $authId 202 | * @return boolean. 203 | */ 204 | public function deleteMessage($messageId, $authId) 205 | { 206 | $message = Message::findOrFail($messageId); 207 | if ($message->sender_id == $authId) { 208 | $message->update(['deleted_from_sender' => 1]); 209 | } else { 210 | $message->update(['deleted_from_receiver' => 1]); 211 | } 212 | 213 | return response()->json(['success' => true], 200); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Models/Conversation.php: -------------------------------------------------------------------------------- 1 | hasMany('BaklySystems\LaravelMessenger\Models\Message', 'conversation_id'); 45 | } 46 | 47 | /** 48 | * Get conversation first user 49 | * 50 | * @return collection 51 | */ 52 | public function userOne() 53 | { 54 | return $this->belongsTo(config('messenger.user.model', 'App\User'), 'user_one'); 55 | } 56 | 57 | /** 58 | * Get conversation second user. 59 | * 60 | * @return collection 61 | */ 62 | public function userTwo() 63 | { 64 | return $this->belongsTo(config('messenger.user.model', 'App\User'), 'user_two'); 65 | } 66 | 67 | /** 68 | * Get conversation last message for threads. 69 | * 70 | * @return collection 71 | */ 72 | public function lastMessage() 73 | { 74 | return $this->messages->last(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Models/Message.php: -------------------------------------------------------------------------------- 1 | 'integer', 30 | 'withId' => 'required|integer', // message reciever. 31 | 'message' => 'required|string', 32 | 'is_seen' => 'boolean', 33 | 'deleted_from_sender' => 'boolean', 34 | 'deleted_from_receiver' => 'boolean' 35 | ]; 36 | 37 | /** 38 | * The rules getter. 39 | * 40 | * @return array 41 | */ 42 | public static function rules() 43 | { 44 | return self::$rules; 45 | } 46 | 47 | /** 48 | * Get message conversation. 49 | * 50 | * @return collection 51 | */ 52 | public function conversation() 53 | { 54 | return $this->belongsTo('BaklySystems\LaravelMessenger\Models\Conversation'); 55 | } 56 | 57 | /** 58 | * Get message sender. 59 | * 60 | * @return collection 61 | */ 62 | public function sender() 63 | { 64 | return $this->belongsTo(config('messenger.user.model', config('messenger.user.model', 'App\User'))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/css/messenger.css: -------------------------------------------------------------------------------- 1 | .messenger { 2 | height: 24em; 3 | overflow-y: scroll; 4 | overflow-x: hidden; 5 | } 6 | 7 | /*Send Box*/ 8 | 9 | #message-body { 10 | width: 100%; 11 | border: 1px solid lightgray; 12 | padding: 10px 20px; 13 | margin-bottom: 10px; 14 | border-radius: 5px; 15 | resize: none; 16 | } 17 | 18 | /*Headers*/ 19 | 20 | .panel-default>.panel-heading { 21 | background-color: #2a88bd; 22 | color: #fff; 23 | } 24 | 25 | /*Profile*/ 26 | 27 | .panel-default>.panel-body p span { 28 | color: #ccc; 29 | font-weight: bold; 30 | } 31 | 32 | /*Messages*/ 33 | 34 | .messenger-body div p { 35 | color: #fff; 36 | padding: 5px 1em; 37 | margin: 3px 1em; 38 | width: fit-content; 39 | width: -moz-fit-content; 40 | max-width: 80%; 41 | border-radius: 5px; 42 | } 43 | 44 | .sent { 45 | background-color: #63b4dc; 46 | float: right; 47 | } 48 | 49 | .received { 50 | background-color: #ccc; 51 | float: left; 52 | } 53 | 54 | .unsent { 55 | color: #f03d25; 56 | float: right; 57 | cursor: pointer; 58 | } 59 | 60 | /*Threads*/ 61 | 62 | .thread-link:hover { 63 | text-decoration: none; 64 | } 65 | 66 | .thread-row { 67 | background-color: #fff; 68 | padding-top: 8px; 69 | } 70 | 71 | .thread-row:hover, .current { 72 | background-color: #f3f3f2; 73 | } 74 | 75 | .thread-user { 76 | color: #000; 77 | padding: 0 1em; 78 | } 79 | 80 | .thread-message { 81 | padding: 0 1em; 82 | color: #999999; 83 | font-size: 13px; 84 | } 85 | 86 | .unseen { 87 | font-weight: bold; 88 | } 89 | .unseen p.thread-message { 90 | color: #333; 91 | } 92 | /*Messages Preloader*/ 93 | 94 | #messages-preloader { 95 | border: 2px solid #f3f3f3; 96 | border-top: 2px solid #3498db; 97 | border-radius: 50%; 98 | width: 20px; 99 | height: 20px; 100 | animation: spin 2s linear infinite; 101 | margin: auto; 102 | } 103 | 104 | @keyframes spin { 105 | 0% { 106 | transform: rotate(0deg); 107 | } 108 | 100% { 109 | transform: rotate(360deg); 110 | } 111 | } 112 | 113 | .start-conv { 114 | text-align: center; 115 | color: #ccc; 116 | } 117 | 118 | /*Menu Dots*/ 119 | 120 | .fa-ellipsis-h { 121 | color: #eee; 122 | display: none; 123 | position: relative; 124 | } 125 | 126 | .fa-ellipsis-h:hover { 127 | cursor: pointer; 128 | } 129 | 130 | .delete { 131 | display: none; 132 | position: absolute; 133 | bottom: 30px; 134 | right: 5px; 135 | background-color: #000; 136 | color: #fff; 137 | opacity: 0.8; 138 | font-size: 18px; 139 | border-radius: 5px; 140 | padding: 10px; 141 | } 142 | -------------------------------------------------------------------------------- /src/assets/js/messenger-chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This JS file applies only for messenger page. 3 | */ 4 | ;(function () { 5 | var take = (messagesCount < 20) ? 0 : 20, // represents the number of messages to be displayed. 6 | $messenger = $('.messenger'), 7 | $loader = $('#messages-preloader'), 8 | channel = pusher.subscribe('messenger-channel'); 9 | 10 | /** 11 | * Do action when a message event is triggered. 12 | */ 13 | channel.bind('messenger-event', function (data) { 14 | if (data.senderId == withId && data.withId == authId && data.withId != data.senderId) { // current conversation thread. 15 | newMessage(data.message, 'received'); 16 | playTweet(); 17 | makeSeen(authId, withId); 18 | } else if (data.withId == authId && data.withId != data.senderId) { // not opened thread. 19 | playTweet(); 20 | loadThreads(); 21 | } 22 | }); 23 | 24 | /** 25 | * Make a message seen. 26 | */ 27 | function makeSeen(authId, withId) { 28 | $.ajax({ 29 | url: '/messenger/ajax/make-seen', 30 | method: 'POST', 31 | data: {authId: authId, withId: withId} 32 | }).done(function (res) { 33 | if (res.success) { 34 | loadThreads(); 35 | } 36 | }); 37 | } 38 | 39 | /** 40 | * Delete confirmation. 41 | */ 42 | function confirmDelete() { 43 | return confirm('Are your sure you want to delete this message'); 44 | } 45 | 46 | /** 47 | * Create a new menu for the new message. 48 | */ 49 | function newMenu(messageClass, messageId) { 50 | var pull = (messageClass === 'sent') ? 'pull-right' : 'pull-left'; 51 | 52 | return '\ 53 | \ 56 | '; 57 | } 58 | 59 | /** 60 | * Append a new message to chat body. 61 | */ 62 | function newMessage(message, messageClass, failed = 0) { 63 | $('.messenger-body').append('\ 64 |
\ 65 |

' + message.message + '

\ 66 | '+ newMenu(messageClass, message.id) +'\ 67 |
\ 68 | '); 69 | if (failed) { 70 | $('.messenger-body').append('\ 71 | \ 72 | \ 73 | This message didn\'t send. Check your internet connection and try again.\ 74 | \ 75 |
\ 76 | '); 77 | } else { 78 | $('#message-body').val(''); 79 | } 80 | scrollMessagesDown(); 81 | } 82 | 83 | /** 84 | * Scroll messages down to some height or bottom. 85 | */ 86 | function scrollMessagesDown(height = 0) { 87 | var scrollTo = height || $messenger.prop('scrollHeight'); 88 | 89 | $messenger.scrollTop(scrollTo); 90 | } 91 | 92 | /** 93 | * Reload threads. 94 | */ 95 | function loadThreads() { 96 | $.ajax({ 97 | url: '/messenger/threads', 98 | method: 'GET', 99 | data: {withId: withId} 100 | }).done(function (view) { 101 | $('.threads').html(view); 102 | }); 103 | } 104 | 105 | /** 106 | * Load more messages. 107 | */ 108 | function loadMessages() { 109 | $.ajax({ 110 | url: '/messenger/more/messages', 111 | method: 'GET', 112 | data: { 113 | withId: withId, 114 | take: take 115 | } 116 | }).done(function (res) { 117 | var prevHeight = $messenger.prop('scrollHeight'); 118 | 119 | $('.messenger-body').html(res.view); 120 | var newHeight = $messenger.prop('scrollHeight'); 121 | scrollMessagesDown(newHeight - prevHeight); // stop at the current height. 122 | if (res.messagesCount < take) { // load no more messages. 123 | take = 0; 124 | $loader.after('

Conversation started

'); 125 | $loader.remove(); 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * Play message notification sound. 132 | */ 133 | function playTweet() { 134 | var audio = new Audio('/vendor/messenger/sounds/tweet.mp3'); 135 | audio.play(); 136 | } 137 | 138 | $(document).ready(function () { 139 | $.ajaxSetup({ 140 | headers: { 141 | 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 142 | } 143 | }); 144 | 145 | scrollMessagesDown(); 146 | 147 | /** 148 | * Send message to backend and handle responses. 149 | */ 150 | $(document).on('click', '#send-btn', function (e) { 151 | var message = $('#message-body').val(); 152 | 153 | if (message) { 154 | var JqHXR = $.ajax({ 155 | url: '/messenger/send', 156 | method: 'POST', 157 | data: { 158 | message: message, 159 | withId: withId 160 | } 161 | }); 162 | } 163 | JqHXR.done(function (res) { // message sent. 164 | if (res.success) { 165 | newMessage(res.message, 'sent'); 166 | loadThreads(); 167 | } 168 | }); 169 | JqHXR.fail(function (res) { // message didn't send. 170 | newMessage(res.message, 'sent', true); 171 | }); 172 | }); 173 | 174 | /** 175 | * Load more messages when scroll to top. 176 | */ 177 | $messenger.on('scroll', function (e) { 178 | if (!$messenger.scrollTop() && take) { 179 | take += 20; 180 | loadMessages(); 181 | } 182 | }); 183 | 184 | /** 185 | * Hover to messages to show menu dots. 186 | */ 187 | $(document).on('mouseover', '.message-row', function (e) { 188 | $(this).find('.fa-ellipsis-h').show(); 189 | }); 190 | 191 | /** 192 | * Mouse up to remove menu dots. 193 | */ 194 | $(document).on('mouseout', '.message-row', function (e) { 195 | var deleteBtn = $(this).find('.delete').css('display'); 196 | 197 | if (deleteBtn !== 'block') { 198 | $(this).find('.fa-ellipsis-h').hide(); // only hide if delete popup is not poped up. 199 | } 200 | }); 201 | 202 | /** 203 | * CLick on menu dots to show up delete message option. 204 | */ 205 | $(document).on('click', '.fa-ellipsis-h', function (e) { 206 | // Hide all other opened menus. 207 | $('.delete').not($(this).find('.delete')).hide(); 208 | $('.fa-ellipsis-h').not($(this).find('.fa-ellipsis-h')).hide(); 209 | // Only show this menu. 210 | $(this).find('.delete').toggle(); 211 | }); 212 | 213 | /** 214 | * Hide opened delete menu when click anywhere. 215 | */ 216 | $('body').on('click', function (e) { 217 | if ($(e.target).hasClass('fa-ellipsis-h')) { // toggle delete menu on clicking on dots. 218 | return true; 219 | } else if ($(e.target).hasClass('message-row')) { // don't hide on hover dots when click on message row. 220 | $('.fa-ellipsis-h').not($(e.target).find('.fa-ellipsis-h')).hide(); 221 | } else { 222 | $('.fa-ellipsis-h').hide(); 223 | } 224 | $('.delete').hide(); 225 | }); 226 | 227 | /** 228 | * Delete message. 229 | */ 230 | $(document).on('click', '.delete', function (e) { 231 | var confirm = confirmDelete(); 232 | if (confirm) { 233 | var id = $(this).attr('data-id'), 234 | $message = $(this).parent().parent(); 235 | 236 | $.ajax({ 237 | url: '/messenger/delete/' + id, 238 | method: 'DELETE' 239 | }).done(function (res) { 240 | if (res.success) { 241 | $message.hide(function () { 242 | $message.remove(); 243 | }); 244 | } 245 | }); 246 | } 247 | }); 248 | }); 249 | }()); 250 | -------------------------------------------------------------------------------- /src/assets/sounds/tweet.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baklysystems/laravel-chat-messenger/65b9c7e896a1f5c3bd8a8b62363895108b88f7d4/src/assets/sounds/tweet.mp3 -------------------------------------------------------------------------------- /src/config/messenger.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'model' => 'App\User' 17 | ], 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Messenger Pusher Keys 22 | |-------------------------------------------------------------------------- 23 | | 24 | | This option defines pusher keys. 25 | | 26 | */ 27 | 28 | 'pusher' => [ 29 | 'app_id' => '', 30 | 'app_key' => '', 31 | 'app_secret' => '', 32 | 'options' => [ 33 | 'cluster' => '', 34 | 'encrypted' => true 35 | ] 36 | ], 37 | ]; 38 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_24_140229_create_conversaions_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('user_one'); 19 | $table->unsignedInteger('user_two'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('conversations'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_24_140250_create_messages_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('conversation_id'); 19 | $table->unsignedInteger('sender_id'); 20 | $table->text('message'); 21 | $table->boolean('is_seen')->default(0); 22 | $table->boolean('deleted_from_sender')->default(0); 23 | $table->boolean('deleted_from_receiver')->default(0); 24 | $table->foreign('conversation_id')->references('id')->on('conversations'); 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 | -------------------------------------------------------------------------------- /src/routes/messenger.php: -------------------------------------------------------------------------------- 1 | group(function () { 8 | Route::get('t/{id}', 'App\Http\Controllers\MessageController@laravelMessenger')->name('messenger'); 9 | Route::post('send', 'App\Http\Controllers\MessageController@store')->name('message.store'); 10 | Route::get('threads', 'App\Http\Controllers\MessageController@loadThreads')->name('threads'); 11 | Route::get('more/messages', 'App\Http\Controllers\MessageController@moreMessages')->name('more.messages'); 12 | Route::delete('delete/{id}', 'App\Http\Controllers\MessageController@destroy')->name('delete'); 13 | // AJAX requests. 14 | Route::prefix('ajax')->group(function () { 15 | Route::post('make-seen', 'App\Http\Controllers\MessageController@makeSeen')->name('make-seen'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/views/messenger.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('css-styles') 4 | 5 | 6 | @endsection 7 | 8 | @section('content') 9 |
10 |
11 |
12 | @include('messenger::partials.threads') 13 |
14 | 15 |
16 |
17 |

{{$withUser->name}}

18 | 19 |
20 |
21 | @if( is_array($messages) ) 22 | @if (count($messages) === 20) 23 |
24 | @endif 25 | 26 |
27 | @else 28 |

Conversation started

29 | @endif 30 |
31 | @include('messenger::partials.messages') 32 |
33 |
34 |
35 | 36 | 41 |
42 |
43 | 44 |
45 |
46 |

Profile

47 | 48 |
49 |

50 | Name {{$withUser->name}} 51 |

52 |

53 | Email {{$withUser->email}} 54 |

55 |
56 |
57 |
58 |
59 |
60 | @endsection 61 | 62 | @section('js-scripts') 63 | 64 | 72 | 73 | @endsection 74 | -------------------------------------------------------------------------------- /src/views/partials/messages.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $authId = auth()->id(); 3 | @endphp 4 | @if ($messages) 5 | @foreach ($messages as $key => $message) 6 |
7 |

sender_id === $authId) 9 | class="sent" 10 | @else 11 | class="received" 12 | @endif> 13 | {{$message->message}} 14 |

15 | @if ($message->sender_id === $authId) 16 | 19 | @else 20 | 23 | @endif 24 |
25 | @endforeach 26 | @endif 27 | -------------------------------------------------------------------------------- /src/views/partials/threads.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

Threads

3 | 4 |
5 | @foreach ($threads as $key => $thread) 6 | @if ($thread->lastMessage) 7 | 8 |
19 |

20 | {{$thread->withUser->name}} 21 |

22 |

23 | @if ($thread->lastMessage->sender_id === auth()->id()) 24 | 25 | @endif 26 | {{substr($thread->lastMessage->message, 0, 20)}} 27 |

28 |
29 |
30 | @endif 31 | @endforeach 32 |
33 |
34 | --------------------------------------------------------------------------------