Pt 6: Build a Conversations System with Laravel and Lucid Architecture
In our last post in the series, we had some fun building out the users listing interface. Today, we'll be up to some more awesomeness!
Tutorial Content
Today's tasks will be a breeze. We will:
- Build out a user interface for displaying conversations involving our user.
- Add methods to our models to keep our code tidy and reusable.
Difficulty
This tutorial is rated: Intermediate.
Requirements
- PHP version 5.6.4 or greater
- Composer version 1.4.1 or greater
- Lucid Laravel version 5.3.*
- Our previous code. Github repo
Straight to it.
Hello friend. Last time, we added some new stuff. We created our test users, setup basic authentication, and built an interface to see all test users using the ListUsersFeature
. We also added model factories and seeders.
Today, we will be building out an interface to help us message other users. We will also add some new helpers to improve the user experience for our conversations system in a little bit. Let's move on.
Setting up.
We need to tidy up a few loose ends before we can proceed. First of all, let's add these convenience methods to our src/Data/Repositories/Repository.php
file.
/**
* Creates a new instance of the model.
* Kinda like our fillAndSave method but we get timestamp updates as perks.
*
* @param array $attributes
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function create($attributes = null)
{
return $this->model->create($attributes);
}
/**
* Creates a new instance of the model if it does not exist.
*
* @param array $attributes
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function createIfNotExists($attributes = null)
{
return $this->model->firstOrCreate($attributes);
}
Building our conversers interface.
Great! We now need to provide the interface for viewing all our conversers. Conversers are users we have established a conversation with. To do this, edit /src/Services/Web/Http/Controllers/ConversationsController.php
. Let's import our features at the top of our file
use App\Services\Web\Features\ListConversationsFeature;
use App\Services\Web\Features\ConversationsFeature;
Add this code to the index
method
return $this->serve(ListConversationsFeature::class);
Add this line to the show
method
return $this->serve(ConversationsFeature::class, [
'id' => $id
]);
Great! Next we have to create the ListConversationsFeature
and ConversationsFeature
. Let's run these commands
C:\xampp\htdocs\conviex\vendor\bin>lucid make:feature ListConversationsFeature web
C:\xampp\htdocs\conviex\vendor\bin>lucid make:feature ConversationsFeature web
Let's add some code to the ListConversationsFeature
. First import these classes
use Auth;
use App\Domains\Conversation\Jobs\FetchUserConversationsJob;
Add these lines to the handle
method.
$conversations = $this->run(FetchUserConversationsJob::class, [
'userID' => Auth::id()
]);
$unreadConversations = collect($conversations)->filter(function ($conversation) {
return $conversation->isUnread(Auth::id());
})->count();
$data = [
'conversations' => $conversations,
'unreadConversations' => $unreadConversations
];
return view('app.conversation.conversation-index', [
'data' => $data
]);
There's something a little odd about this code. We're referencing the FetchUsersJob
class. This job doesn't exist yet so let's create it.
C:\xampp\htdocs\conviex\vendor\bin>lucid make:job FetchUserConversationsJob conversation
Add these lines to the newly created job. You can find it at /src/Domains/Conversation/Jobs/FetchUserConversationsJob.php
. First of all, import the Converser
Eloquent model.
use Framework\Converser;
Next, add the constructor method
public function __construct($userID)
{
$this->userID = $userID;
}
Then add these to the handle
method
$conversations = [];
$converserInstances = Converser::where('user_id', $this->userID)->get();
foreach ($converserInstances as $converser) {
foreach ($converser->conversations as $conversation)
{
$conversations[] = $conversation;
}
}
return $conversations;
Great! One more thing—the Conversation
model currently does not have the utility method isUnread
. Let's add all utility methods for our Conversation
model.
Open up app/Conversation.php
and add these lines to the body of the Conversation
class
/*
* Every conversation has messages.
*
* @return Eloquent Object
*/
public function messages()
{
return $this->hasMany('Framework\Message');
}
/*
* Return the last message of a conversation.
*
* @return Eloquent Object
*/
public function getLastMessage()
{
return $this->messages()->latest()->first();
}
/**
* Returns array of unread messages in conversation for given converser.
*
* @param $userId
*
* @return \Illuminate\Support\Collection
*/
public function userUnreadMessages($userId)
{
$messages = $this->messages()->get();
try
{
$converser = $this->getConverserFromUser($userId);
} catch (ModelNotFoundException $e)
{
return collect();
}
if (!$converser->last_read_at)
{
return $messages;
}
return $messages->filter(function ($message) use ($converser){
return $message->updated_at->gt($converser->last_read_at);
});
}
/**
* Finds the converser record from a user id.
*
* @param $userId
*
* @return mixed
*
* @throws ModelNotFoundException
*/
public function getConverserFromUser($userId)
{
return $this->conversers()->where('user_id', $userId)->firstOrFail();
}
/**
* Returns count of unread messages in conversation for a given user.
*
* @param $userId
*
* @return int
*/
public function userUnreadMessagesCount($userId)
{
return $this->userUnreadMessages($userId)->count();
}
/**
* See if the current conversation is unread by the user.
*
* @param int $userId
*
* @return bool
*/
public function isUnread($userId)
{
try {
$converser = $this->getConverserFromUser($userId);
if ($converser->last_read_at === null || $this->updated_at->gt($converser->last_read_at)) {
return true;
}
} catch (ModelNotFoundException $e) {
// do nothing
}
return false;
}
/*
* Return the last message of a conversation.
*
* @return Eloquent Object
*/
public function getLastMessageHash()
{
if (!count($this->getLastMessage())) return;
return '#message-'.$this->getLastMessage()->id;
}
/*
* Return the last message of a conversation.
*
* @return Eloquent Object
*/
public function getOtherConverser($userID)
{
return collect($this->conversers->all())->reject(function ($converser) use ($userID) {
return $converser->user_id == $userID;
})->first()->user;
}
public function getTimeDiff()
{
$createdAt = $this->created_at;
if ($createdAt->diffInSeconds() < 60) return $createdAt->diffInSeconds(). 's';
if ($createdAt->diffInMinutes() < 60) return $createdAt->diffInMinutes(). 'm';
if ($createdAt->diffInHours() < 24) return $createdAt->diffInHours(). 'h';
return $createdAt->format('jS M Y');
}
Next, add this utility method to app/User.php
.
/*
* Returns a user's first name.
*
* @return { String }
*/
public function getFirstName()
{
$str = explode('.', trim($this->name));
$name = count($str) > 1 ? $str[1] : $str[0];
$name = explode(' ', trim($name))[0];
return $name;
}
We're doing awesome! Next we need to provide some markup for our conversation-index.blade.php
view. Create a file named conversation-index.blade.php
at /resources/views/app/conversation
and add this markup
@extends('layouts.app')
@section('view-title')
{{ $data['unreadConversations'] ? '('. $data['unreadConversations']. ')' : '' }} Conversations · FashLogue
@endsection
@section('content')
@if (count($data['conversations']))
<ul class="list list-unstyled col-sm-10 col-sm-offset-1">
<h2 class="h4" style="margin-bottom: 30px;">Your Messages</h2>
@foreach($data['conversations'] as $conversation)
<li class="list-item">
<a class="media" style="display: block;" href="{{ url(route('conversation', $conversation->id). $conversation->getLastMessageHash()) }}">
<div class="media-left">
<img src="{{ $conversation->getOtherConverser(Auth::id())->getUserAvatar(40) }}" alt="" class="img img-circle">
</div>
<div class="media-body">
<div class="col-xs-10">
<h3 class="h4 media-heading">{{ $conversation->getOtherConverser(Auth::id())->name }}</h3>
<p class="text-muted">
@if($conversation->getLastMessage() && $conversation->getLastMessage()->user_id == Auth::id())
<span class="u-weight--bold text-muted">You:</span>
@endif
{!! $conversation->getLastMessage() ?
$conversation->getLastMessage()->body :
'<i class="glyphicon glyphicon-envelope"></i> Message'
!!}
</p>
<hr class="hidden-xs">
</div>
<div class="col-xs-2">
<time class="text-muted small text-right">
{{ $conversation->getLastMessage() ? $conversation->getLastMessage()->getTimeDiff() : $conversation->getTimeDiff() }}
</time>
</div>
</div>
</a>
</li>
@endforeach
</ul>
@else
<div class="container-fluid">
<section class="row">
<div class="col-md-10 col-md-offset-1">
<h2 class="h3" style="margin-bottom: 30px;">Let's get you started</h2>
<p class="lead text-muted">Oops. Your conversations list is a little sad at the moment. You could <a href="{{ url(route('users')) }}">start a conversation now</a></p>
</div>
</section>
</div>
@endif
</div>
</section>
</div>
</section>
</div>
</section>
Also create a file at /public/css
called custom.css
and add these styles
.card
{
padding: 10px;
border-radius: 15px;
width: 100%;
}
.card--default
{
background: white;
box-shadow: 1px 1.732px 20px 0px #efefef;
}
Next, open up /resources/views/layout/app.php
and add this line in the head
section.
<link href="/css/custom.css" rel="stylesheet">
Let's check it out. Visit http://localhost:8000/conversations
and you should see this message.
This is a little disappointing. We don't have any conversations at the moment. Let's fix that. Simply visit https://localhost:8000/users
and tap the message button on a user listing to start a conversation with that user.
Great! We see an empty page but don't be afraid. We're only seeing that because we're yet to build out the ConversersFeature
. We'll build out the ConversersFeature
in the next post.
Now visit http://localhost:8000/conversations
and you should see a page that looks like this
That's awesome. We now have a list of all the users we've ever had a conversation with.
Conclusion
In this post, we revisited some important content from our previous post in this series. We generated the ConversationsFeature
and the ListConversationsFeature
. We also established some conversations with our test users and built an interface to help us see all users we have a conversation with.
Our next post will be really awesome, guys! We'll do so much new stuff. We will focus on building the interface and implementation for the core of the conversations system. We will also create some new methods and so much more.
Keep on the edges of your chairs. The next installment will be steaming hot.
Curriculum
Posted on Utopian.io - Rewarding Open Source Contributors
@creatrixity, Contribution to open source project, I like you and upvote.
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @creatrixity I am @utopian-io. I have just upvoted you!
Achievements
Suggestions
Get Noticed!
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x