Skip to main content

Overview

Messages are the individual communications within a conversation. They can be text-based or include attachments, and support features like replies, read receipts, and deletion.

Message Types

WireChat supports two message types:
use Wirechat\Wirechat\Enums\MessageType;

enum MessageType: string
{
    case TEXT = 'text';
    case ATTACHMENT = 'attachment';
}

Text Messages

Standard text-based messages:
$message = Message::create([
    'body' => 'Hello, world!',
    'type' => MessageType::TEXT,
    'participant_id' => $participant->id,
    'conversation_id' => $conversation->id,
]);

Attachment Messages

Messages with file attachments:
$message = Message::create([
    'body' => 'Check out this file',
    'type' => MessageType::ATTACHMENT,
    'participant_id' => $participant->id,
    'conversation_id' => $conversation->id,
]);

// Check if message has attachment
if ($message->isAttachment()) {
    $attachment = $message->attachment;
}

Database Structure

Messages have the following key attributes:
/**
 * @property int $id
 * @property int|null $conversation_id
 * @property int $participant_id
 * @property int|null $reply_id
 * @property string|null $body
 * @property MessageType $type
 * @property \Illuminate\Support\Carbon|null $kept_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 */
Messages use soft deletes (deleted_at) to allow for recovery and maintain reply chains.

Relationships

Conversation

Every message belongs to a conversation:
$conversation = $message->conversation;

Participant

Messages are sent by participants:
// Get the participant who sent the message
$participant = $message->participant;

// Get the actual user (through the participant)
$user = $message->user;
The user accessor provides direct access to the underlying user model via the participant relationship.

Attachment

Messages can have one attachment:
// Check if message has attachment
if ($message->hasAttachment()) {
    $attachment = $message->attachment;
    echo $attachment->url;
}

Reply Chain

Messages can be replies to other messages:
// Create a reply
$reply = Message::create([
    'body' => 'This is a reply',
    'participant_id' => $participant->id,
    'conversation_id' => $conversation->id,
    'reply_id' => $originalMessage->id,
]);

// Get the parent message
$parent = $reply->parent;

// Check if message has a parent
if ($message->hasParent()) {
    echo "This is a reply to: " . $message->parent->body;
}

// Check if message has replies
if ($message->hasReply()) {
    echo "This message has been replied to";
}

Creating Messages

Create messages with all required attributes:
use Wirechat\Wirechat\Models\Message;
use Wirechat\Wirechat\Enums\MessageType;

$message = Message::create([
    'body' => 'Hello from WireChat!',
    'type' => MessageType::TEXT,
    'participant_id' => $participant->id,
    'conversation_id' => $conversation->id,
]);

Ownership

Check if a message belongs to a specific user:
// Check ownership
if ($message->ownedBy($user)) {
    echo "This message belongs to the user";
}

// Check if belongs to authenticated user
if ($message->belongsToAuth()) {
    echo "This message belongs to the authenticated user";
}

Read Status

Check if a message has been read:
if ($message->readBy($user)) {
    echo "Message has been read";
}
Read status is tracked at the conversation level via the participant’s conversation_read_at timestamp.

Deleting Messages

Delete for User

Delete a message for a specific user only:
$message->deleteFor($user);
The deleteFor() method:
  • Creates a DELETE action tied to the user’s participant
  • For private conversations, permanently deletes if both participants delete it
  • For self conversations, immediately force deletes
public function deleteFor(Model|Authenticatable $user)
{
    $conversation = $this->conversation;
    abort_unless($user->belongsToConversation($conversation), 403);

    if ($conversation->isSelf()) {
        return $this->forceDelete();
    }

    $actorParticipant = $conversation->participant($user);
    abort_unless($actorParticipant != null, 403, 'You do not belong to this conversation');

    $this->actions()->create([
        'actor_id' => $actorParticipant->getKey(),
        'actor_type' => $actorParticipant->getMorphClass(),
        'type' => Actions::DELETE,
    ]);

    // Check if both participants deleted in private conversation
    if ($conversation->isPrivate()) {
        // ... deletion logic
    }

    return null;
}

Delete for Everyone

Permanently delete a message for all participants:
$message->deleteForEveryone($user);
Only the message owner or group admins can delete messages for everyone. Messages with replies are soft-deleted to maintain the reply chain.

Emoji Detection

Check if a message contains only emojis:
if ($message->isEmoji()) {
    // Render as large emoji
    echo "<div class='emoji-only'>" . $message->body . "</div>";
}

Query Scopes

whereIsNotOwnedBy

Find messages not owned by a specific user:
$messages = Message::whereIsNotOwnedBy($user)->get();
This is useful for finding unread messages:
$unreadMessages = $conversation->messages()
    ->whereIsNotOwnedBy($user)
    ->where('created_at', '>', $participant->conversation_read_at)
    ->get();

Kept Messages

Messages can be marked as “kept” to prevent them from disappearing:
$message->update(['kept_at' => now()]);

if ($message->kept_at !== null) {
    echo "This message is kept and won't disappear";
}
The kept_at timestamp is used with disappearing messages to exclude specific messages from auto-deletion.

Global Scopes

Messages use the WithoutRemovedMessages global scope to automatically filter out messages deleted by the current user:
protected static function booted()
{
    static::addGlobalScope(WithoutRemovedMessages::class);
}
To include removed messages, remove the scope:
$allMessages = Message::withoutGlobalScope(WithoutRemovedMessages::class)->get();

Cascade Deletion

When a message is deleted, its associated data is automatically cleaned up:
static::deleted(function ($message) {
    if ($message->attachment?->exists()) {
        $message->attachment->delete();
    }

    DB::transaction(function () use ($message) {
        $message->actions()->delete();
    });
});

Sendable Deprecation

The sendable relationship is deprecated. Use the user accessor instead:
// ❌ Deprecated
$sender = $message->sendable;

// ✅ Use this
$sender = $message->user;

Best Practices

  • Always set the correct MessageType when creating messages
  • Use deleteFor() for user-specific deletion to preserve messages for others
  • Check message ownership before allowing deletion or editing
  • Use soft deletes to maintain reply chains
  • Load attachments eagerly when querying multiple messages
  • Check isEmoji() to apply special styling for emoji-only messages

Common Patterns

Sending a Text Message

$message = Message::create([
    'body' => $request->message,
    'type' => MessageType::TEXT,
    'participant_id' => $conversation->participant($user)->id,
    'conversation_id' => $conversation->id,
]);

Replying to a Message

$reply = Message::create([
    'body' => $request->reply,
    'type' => MessageType::TEXT,
    'participant_id' => $participant->id,
    'conversation_id' => $conversation->id,
    'reply_id' => $parentMessage->id,
]);

Getting Unread Messages

$unreadCount = $conversation->messages()
    ->whereIsNotOwnedBy(auth()->user())
    ->where('created_at', '>', $participant->conversation_read_at ?? now())
    ->count();

Next Steps

Attachments

Learn about file attachments and uploads

Participants

Understand participant roles and permissions