Skip to content

Commit 8c12aa3

Browse files
authored
feat: Add message reactions support (#352)
1 parent 54a5237 commit 8c12aa3

11 files changed

Lines changed: 680 additions & 0 deletions

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Create a Chat application for your multiple Models
3131
- [Mark whole conversation as read](#mark-whole-conversation-as-read)
3232
- [Unread messages count](#unread-messages-count)
3333
- [Delete a message](#delete-a-message)
34+
- [Message Reactions](#message-reactions)
3435
- [Cleanup Deleted Messages](#cleanup-deleted-messages)
3536
- [Clear a conversation](#clear-a-conversation)
3637
- [Get participant conversations](#Get-participant-conversations)
@@ -256,6 +257,66 @@ Chat::conversation($conversation)->setParticipant($participantModel)->unreadCoun
256257
Chat::message($message)->setParticipant($participantModel)->delete();
257258
```
258259

260+
#### Message Reactions
261+
262+
Add emoji or text-based reactions to messages:
263+
264+
```php
265+
// Add a reaction
266+
Chat::message($message)->setParticipant($participantModel)->react('👍');
267+
268+
// Add multiple different reactions
269+
Chat::message($message)->setParticipant($participantModel)->react('❤️');
270+
```
271+
272+
Remove a reaction:
273+
274+
```php
275+
Chat::message($message)->setParticipant($participantModel)->unreact('👍');
276+
```
277+
278+
Toggle a reaction (add if not present, remove if present):
279+
280+
```php
281+
$result = Chat::message($message)->setParticipant($participantModel)->toggleReaction('👍');
282+
// $result = ['added' => true/false, 'reaction' => Reaction|null]
283+
```
284+
285+
Get reactions summary with counts:
286+
287+
```php
288+
$summary = Chat::message($message)->reactionsSummary();
289+
// ['👍' => 5, '❤️' => 3, '😂' => 1]
290+
```
291+
292+
Check if participant has reacted:
293+
294+
```php
295+
// Check for specific reaction
296+
Chat::message($message)->setParticipant($participantModel)->hasReacted('👍');
297+
298+
// Check for any reaction
299+
Chat::message($message)->setParticipant($participantModel)->hasReacted();
300+
```
301+
302+
Get all reactions on a message:
303+
304+
```php
305+
$reactions = Chat::message($message)->reactions();
306+
```
307+
308+
You can also access reactions directly on the Message model:
309+
310+
```php
311+
$message->reactions; // All reactions
312+
$message->getReactionsSummary(); // Grouped counts
313+
$message->react($participant, '👍'); // Add
314+
$message->unreact($participant, '👍'); // Remove
315+
$message->hasReacted($participant, '👍'); // Check
316+
```
317+
318+
**Broadcasting**: When broadcasting is enabled, `MessageReactionAdded` and `MessageReactionRemoved` events are broadcast to the conversation channel.
319+
259320
#### Cleanup Deleted Messages
260321

261322
What to cleanup when all participants have deleted a `$message` or `$conversation`?
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
use Musonza\Chat\ConfigurationManager;
7+
8+
class AddReactionsToMessages extends Migration
9+
{
10+
protected function schema()
11+
{
12+
$connection = config('musonza_chat.database_connection');
13+
14+
return $connection ? Schema::connection($connection) : Schema::getFacadeRoot();
15+
}
16+
17+
/**
18+
* Run the migrations.
19+
*
20+
* @return void
21+
*/
22+
public function up()
23+
{
24+
$this->schema()->create(ConfigurationManager::REACTIONS_TABLE, function (Blueprint $table) {
25+
$table->bigIncrements('id');
26+
$table->bigInteger('message_id')->unsigned();
27+
$table->bigInteger('messageable_id')->unsigned();
28+
$table->string('messageable_type');
29+
$table->string('reaction', 50); // emoji or reaction type (e.g., '👍', 'like', 'heart')
30+
$table->timestamps();
31+
32+
// Each participant can only have one reaction of each type per message
33+
$table->unique(
34+
['message_id', 'messageable_id', 'messageable_type', 'reaction'],
35+
'unique_reaction_per_user'
36+
);
37+
38+
$table->index(['message_id'], 'reactions_message_index');
39+
$table->index(['messageable_id', 'messageable_type'], 'reactions_messageable_index');
40+
41+
$table->foreign('message_id')
42+
->references('id')
43+
->on(ConfigurationManager::MESSAGES_TABLE)
44+
->onDelete('cascade');
45+
});
46+
}
47+
48+
/**
49+
* Reverse the migrations.
50+
*
51+
* @return void
52+
*/
53+
public function down()
54+
{
55+
$this->schema()->dropIfExists(ConfigurationManager::REACTIONS_TABLE);
56+
}
57+
}

src/ChatServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ public function publishMigrations()
6565
$encryptionStub = __DIR__ . '/../database/migrations/add_is_encrypted_to_messages_table.php';
6666
$encryptionTarget = $this->app->databasePath() . '/migrations/' . $timestamp . '_add_is_encrypted_to_messages_table.php';
6767

68+
$reactionsStub = __DIR__ . '/../database/migrations/add_reactions_to_messages.php';
69+
$reactionsTarget = $this->app->databasePath() . '/migrations/' . $timestamp . '_add_reactions_to_messages.php';
70+
6871
$this->publishes([
6972
$stub => $target,
7073
$encryptionStub => $encryptionTarget,
74+
$reactionsStub => $reactionsTarget,
7175
], 'chat.migrations');
7276
}
7377

src/ConfigurationManager.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class ConfigurationManager
1212

1313
const PARTICIPATION_TABLE = 'chat_participation';
1414

15+
const REACTIONS_TABLE = 'chat_message_reactions';
16+
1517
public static function paginationDefaultParameters()
1618
{
1719
$pagination = config('musonza_chat.pagination', []);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Musonza\Chat\Eventing;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Broadcasting\PrivateChannel;
7+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
8+
use Illuminate\Foundation\Events\Dispatchable;
9+
use Illuminate\Queue\SerializesModels;
10+
use Musonza\Chat\Models\Reaction;
11+
12+
class MessageReactionAdded implements ShouldBroadcast
13+
{
14+
use Dispatchable, InteractsWithSockets, SerializesModels;
15+
16+
/**
17+
* @var Reaction
18+
*/
19+
public $reaction;
20+
21+
/**
22+
* Create a new event instance.
23+
*/
24+
public function __construct(Reaction $reaction)
25+
{
26+
$this->reaction = $reaction;
27+
}
28+
29+
/**
30+
* Get the channels the event should broadcast on.
31+
*
32+
* @return \Illuminate\Broadcasting\Channel|array
33+
*/
34+
public function broadcastOn()
35+
{
36+
return new PrivateChannel('mc-chat-conversation.' . $this->reaction->message->conversation_id);
37+
}
38+
39+
/**
40+
* Get the data to broadcast.
41+
*
42+
* @return array
43+
*/
44+
public function broadcastWith()
45+
{
46+
return [
47+
'reaction' => [
48+
'id' => $this->reaction->id,
49+
'message_id' => $this->reaction->message_id,
50+
'reaction' => $this->reaction->reaction,
51+
'user' => [
52+
'id' => $this->reaction->messageable_id,
53+
'type' => $this->reaction->messageable_type,
54+
],
55+
],
56+
];
57+
}
58+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Musonza\Chat\Eventing;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Broadcasting\PrivateChannel;
7+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
8+
use Illuminate\Foundation\Events\Dispatchable;
9+
use Illuminate\Queue\SerializesModels;
10+
11+
class MessageReactionRemoved implements ShouldBroadcast
12+
{
13+
use Dispatchable, InteractsWithSockets, SerializesModels;
14+
15+
/**
16+
* @var int
17+
*/
18+
public $messageId;
19+
20+
/**
21+
* @var int
22+
*/
23+
public $conversationId;
24+
25+
/**
26+
* @var string
27+
*/
28+
public $reaction;
29+
30+
/**
31+
* @var int
32+
*/
33+
public $messageableId;
34+
35+
/**
36+
* @var string
37+
*/
38+
public $messageableType;
39+
40+
/**
41+
* Create a new event instance.
42+
*/
43+
public function __construct(int $messageId, int $conversationId, string $reaction, int $messageableId, string $messageableType)
44+
{
45+
$this->messageId = $messageId;
46+
$this->conversationId = $conversationId;
47+
$this->reaction = $reaction;
48+
$this->messageableId = $messageableId;
49+
$this->messageableType = $messageableType;
50+
}
51+
52+
/**
53+
* Get the channels the event should broadcast on.
54+
*
55+
* @return \Illuminate\Broadcasting\Channel|array
56+
*/
57+
public function broadcastOn()
58+
{
59+
return new PrivateChannel('mc-chat-conversation.' . $this->conversationId);
60+
}
61+
62+
/**
63+
* Get the data to broadcast.
64+
*
65+
* @return array
66+
*/
67+
public function broadcastWith()
68+
{
69+
return [
70+
'message_id' => $this->messageId,
71+
'reaction' => $this->reaction,
72+
'user' => [
73+
'id' => $this->messageableId,
74+
'type' => $this->messageableType,
75+
],
76+
];
77+
}
78+
}

0 commit comments

Comments
 (0)