Pt 6: Build a Conversations System with Laravel and Lucid Architecture

in #utopian-io6 years ago (edited)

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> &nbsp; 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>

@endsection

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.

Conviex empty conversations list

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

Conviex conversations list with conversers

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

Sort:  

@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

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

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

Coin Marketplace

STEEM 0.26
TRX 0.11
JST 0.033
BTC 63994.25
ETH 3062.19
USDT 1.00
SBD 3.94