Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Create a Chat application for your multiple Models
- [Remove participants from a conversation](#remove-participants-from-a-conversation)
- [Add participants to a conversation](#add-participants-to-a-conversation)
- [Get messages in a conversation](#get-messages-in-a-conversation)
- [Get messages with cursor pagination](#get-messages-with-cursor-pagination)
- [Get public conversations for discovery](#get-public-conversations-for-discovery)
- [Get recent messages](#get-recent-messages)
- [Get participants in a conversation](#get-participants-in-a-conversation)
Expand Down Expand Up @@ -322,6 +323,44 @@ Chat::conversation($conversation)->addParticipants([$participantModel, $particip
Chat::conversation($conversation)->setParticipant($participantModel)->getMessages()
```

#### Get messages with cursor pagination

For real-time chat applications, cursor-based pagination is recommended over offset-based pagination. This prevents duplicate messages when new messages arrive between page loads.

```php
// Get first page
$messages = Chat::conversation($conversation)
->setParticipant($participantModel)
->setCursorPaginationParams([
'perPage' => 25,
'sorting' => 'asc',
])
->getMessagesWithCursor();

// Get next page using cursor from previous response
$nextCursor = $messages->nextCursor()?->encode();

$moreMessages = Chat::conversation($conversation)
->setParticipant($participantModel)
->setCursorPaginationParams([
'perPage' => 25,
'sorting' => 'asc',
'cursor' => $nextCursor,
])
->getMessagesWithCursor();
```

**API Endpoint:** `GET /conversations/{id}/messages-cursor`

Query parameters:
- `participant_id` (required)
- `participant_type` (required)
- `perPage` (optional, default: 25)
- `sorting` (optional: `asc` or `desc`)
- `cursor` (optional - from previous response's `next_cursor`)

The response includes `next_cursor` and `prev_cursor` for navigation.

#### Get user conversations by type

```php
Expand Down Expand Up @@ -380,6 +419,8 @@ You don't have to specify all the parameters. If you leave the parameters out, d
`$paginated` above will return `Illuminate\Pagination\LengthAwarePaginator`
To get the `conversations` simply call `$paginated->items()`

> **Tip:** For paginating messages in real-time chat applications, consider using [cursor pagination](#get-messages-with-cursor-pagination) instead. Cursor pagination prevents duplicate messages when new messages arrive between page loads.


#### Get participants in a conversation

Expand Down
22 changes: 22 additions & 0 deletions src/Http/Controllers/ConversationMessageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Musonza\Chat\Http\Requests\ClearConversation;
use Musonza\Chat\Http\Requests\DeleteMessage;
use Musonza\Chat\Http\Requests\GetParticipantMessages;
use Musonza\Chat\Http\Requests\GetParticipantMessagesWithCursor;
use Musonza\Chat\Http\Requests\StoreMessage;

class ConversationMessageController extends Controller
Expand Down Expand Up @@ -48,6 +49,27 @@ public function index(GetParticipantMessages $request, $conversationId)
return response($message);
}

/**
* Get messages using cursor-based pagination.
*
* Cursor pagination is more suitable for real-time chat applications
* as it avoids duplicate messages when new messages arrive between page loads.
*/
public function indexWithCursor(GetParticipantMessagesWithCursor $request, $conversationId)
{
$conversation = Chat::conversations()->getById($conversationId);
$messages = Chat::conversation($conversation)
->setParticipant($request->getParticipant())
->setCursorPaginationParams($request->getCursorPaginationParams())
->getMessagesWithCursor();

if ($this->messageTransformer) {
return fractal($messages, $this->messageTransformer)->respond();
}

return response($messages);
}

public function show(GetParticipantMessages $request, $conversationId, $messageId)
{
$message = Chat::messages()->getById($messageId);
Expand Down
48 changes: 48 additions & 0 deletions src/Http/Requests/GetParticipantMessagesWithCursor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Musonza\Chat\Http\Requests;

use Musonza\Chat\ValueObjects\Pagination;

class GetParticipantMessagesWithCursor extends BaseRequest
{
/**
* @var Pagination
*/
private $pagination;

public function __construct(Pagination $pagination)
{
parent::__construct();
$this->pagination = $pagination;
}

public function authorized()
{
return true;
}

public function rules()
{
return [
'participant_id' => 'required',
'participant_type' => 'required',
'perPage' => 'integer',
'sorting' => 'string|in:asc,desc',
'columns' => 'array',
'cursor' => 'string|nullable',
'cursorName' => 'string',
];
}

public function getCursorPaginationParams()
{
return [
'perPage' => $this->perPage ?? $this->pagination->getPerPage(),
'sorting' => $this->sorting ?? $this->pagination->getSorting(),
'columns' => $this->columns ?? $this->pagination->getColumns(),
'cursor' => $this->cursor ?? null,
'cursorName' => $this->cursorName ?? 'cursor',
];
}
}
2 changes: 2 additions & 0 deletions src/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
->name('conversations.messages.store');
Route::get('/conversations/{id}/messages', 'ConversationMessageController@index')
->name('conversations.messages.index');
Route::get('/conversations/{id}/messages-cursor', 'ConversationMessageController@indexWithCursor')
->name('conversations.messages.index.cursor');
Route::get('/conversations/{id}/messages/{message_id}', 'ConversationMessageController@show')
->name('conversations.messages.show');
Route::delete('/conversations/{id}/messages', 'ConversationMessageController@deleteAll')
Expand Down
47 changes: 47 additions & 0 deletions src/Models/Conversation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Musonza\Chat\Models;

use Chat;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
Expand Down Expand Up @@ -89,6 +90,21 @@ public function getMessages(Model $participant, $paginationParams, $deleted = fa
return $this->getConversationMessages($participant, $paginationParams, $deleted);
}

/**
* Get messages for a conversation using cursor-based pagination.
*
* Cursor pagination is more suitable for real-time chat applications
* as it avoids duplicate messages when new messages arrive between page loads.
*
* @param array $paginationParams
* @param bool $deleted
* @return CursorPaginator
*/
public function getMessagesWithCursor(Model $participant, $paginationParams, $deleted = false)
{
return $this->getConversationMessagesWithCursor($participant, $paginationParams, $deleted);
}

public function getParticipantConversations($participant, array $options)
{
return $this->getConversationsList($participant, $options);
Expand Down Expand Up @@ -324,6 +340,37 @@ private function getConversationMessages(Model $participant, $paginationParams,
return $messages;
}

/**
* Get messages in conversation using cursor-based pagination.
*
* @return CursorPaginator
*/
private function getConversationMessagesWithCursor(Model $participant, $paginationParams, $deleted)
{
$messages = $this->messages()
->join($this->tablePrefix . 'message_notifications', $this->tablePrefix . 'message_notifications.message_id', '=', $this->tablePrefix . 'messages.id')
->where($this->tablePrefix . 'message_notifications.messageable_type', $participant->getMorphClass())
->where($this->tablePrefix . 'message_notifications.messageable_id', $participant->getKey());
$messages = $deleted ? $messages->whereNotNull($this->tablePrefix . 'message_notifications.deleted_at') : $messages->whereNull($this->tablePrefix . 'message_notifications.deleted_at');
$messages = $messages->orderBy($this->tablePrefix . 'messages.id', $paginationParams['sorting'])
->cursorPaginate(
$paginationParams['perPage'],
[
$this->tablePrefix . 'message_notifications.updated_at as read_at',
$this->tablePrefix . 'message_notifications.deleted_at as deleted_at',
$this->tablePrefix . 'message_notifications.messageable_id',
$this->tablePrefix . 'message_notifications.id as notification_id',
$this->tablePrefix . 'message_notifications.is_seen',
$this->tablePrefix . 'message_notifications.is_sender',
$this->tablePrefix . 'messages.*',
],
$paginationParams['cursorName'] ?? 'cursor',
$paginationParams['cursor'] ?? null
);

return $messages;
}

/**
* @return mixed
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Services/ConversationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ public function getMessages()
return $this->conversation->getMessages($this->participant, $this->getPaginationParams(), $this->deleted);
}

/**
* Get messages in a conversation using cursor-based pagination.
*
* Cursor pagination is more suitable for real-time chat applications
* as it avoids duplicate messages when new messages arrive between page loads.
*
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function getMessagesWithCursor()
{
return $this->conversation->getMessagesWithCursor($this->participant, $this->getCursorPaginationParams(), $this->deleted);
}

/**
* Clears conversation.
*/
Expand Down
44 changes: 44 additions & 0 deletions src/Traits/Paginates.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ trait Paginates

protected $deleted = false;

protected $cursor = null;

protected $cursorName = 'cursor';

/**
* Set the limit.
*
Expand Down Expand Up @@ -75,4 +79,44 @@ public function getPaginationParams()
'pageName' => $this->pageName,
];
}

public function getCursorPaginationParams()
{
return [
'perPage' => $this->perPage,
'sorting' => $this->sorting,
'columns' => $this->columns,
'cursor' => $this->cursor,
'cursorName' => $this->cursorName,
];
}

/**
* Set the cursor for cursor-based pagination.
*
* @param string|null $cursor
* @return $this
*/
public function cursor($cursor)
{
$this->cursor = $cursor;

return $this;
}

/**
* Set cursor pagination parameters.
*
* @return $this
*/
public function setCursorPaginationParams($params)
{
foreach ($params as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}

return $this;
}
}
Loading