• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RonasIT / laravel-chat / 27742063715

18 Jun 2026 06:50AM UTC coverage: 99.658% (-0.2%) from 99.811%
27742063715

push

github

web-flow
Merge pull request #65 from RonasIT/PRD-2238/title-and-cover-for-private-conversations-from-the-other-participant

feat: resolve title and cover for private conversations from the other participant

70 of 71 new or added lines in 11 files covered. (98.59%)

583 of 585 relevant lines covered (99.66%)

31.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.41
/src/Models/Conversation.php
1
<?php
2

3
namespace RonasIT\Chat\Models;
4

5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Casts\Attribute;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
10
use Illuminate\Database\Eloquent\Relations\HasMany;
11
use Illuminate\Database\Eloquent\Relations\HasOne;
12
use Illuminate\Support\Arr;
13
use RonasIT\Chat\Contracts\Models\ConversationModelContract;
14
use RonasIT\Chat\Contracts\Models\MessageModelContract;
15
use RonasIT\Chat\Enums\Conversation\TypeEnum;
16
use RonasIT\Support\Traits\ModelTrait;
17

18
class Conversation extends Model implements ConversationModelContract
19
{
20
    use ModelTrait;
21

22
    protected $fillable = [
23
        'creator_id',
24
        'type',
25
        'title',
26
        'cover_id',
27
        'last_updated_at',
28
    ];
29

30
    protected $hidden = ['pivot'];
31

32
    protected $casts = [
33
        'type' => TypeEnum::class,
34
    ];
35

36
    public function last_message(): HasOne
37
    {
38
        return $this->hasOne(app()->getAlias(MessageModelContract::class))->latest();
18 ✔
39
    }
40

41
    public function messages(): HasMany
42
    {
43
        return $this->hasMany(app()->getAlias(MessageModelContract::class));
21 ✔
44
    }
45

46
    public function creator(): BelongsTo
47
    {
48
        return $this->belongsTo(config('chat.classes.user.model'));
6 ✔
49
    }
50

51
    public function members(): BelongsToMany
52
    {
53
        return $this->belongsToMany(
95 ✔
54
            related: config('chat.classes.user.model'),
95 ✔
55
            table: 'conversation_member',
95 ✔
56
            foreignPivotKey: 'conversation_id',
95 ✔
57
            relatedPivotKey: 'member_id',
95 ✔
58
        );
95 ✔
59
    }
60

61
    public function cover(): BelongsTo
62
    {
63
        return $this->belongsTo(config('chat.classes.media.model'), 'cover_id');
8 ✔
64
    }
65

66
    protected function title(): Attribute
67
    {
68
        return Attribute::make(
47 ✔
69
            get: function ($value): ?string {
47 ✔
70
                return ($this->isPrivate())
47 ✔
71
                    ? Arr::get($this->attributes, 'calculated_title')
45 ✔
72
                    : $value;
47 ✔
73
            },
47 ✔
74
        );
47 ✔
75
    }
76

77
    protected function coverId(): Attribute
78
    {
79
        return Attribute::make(
8 ✔
80
            get: function ($value): ?int {
8 ✔
81
                return ($this->isPrivate())
8 ✔
82
                    ? Arr::get($this->attributes, 'calculated_cover_id')
8 ✔
83
                    : $value;
8 ✔
84
            },
8 ✔
85
        );
8 ✔
86
    }
87

88
    public function scopeWithCalculatedIdentity(Builder $query, int $forMemberId): Builder
89
    {
90
        $constraint = fn (Builder $query) => $query->where('member_id', '!=', $forMemberId);
45 ✔
91

92
        if ($titleColumns = config('chat.classes.user.columns.full_name')) {
45 ✔
93
            $titleColumn = $this->buildTitleExpression($titleColumns);
45 ✔
94

95
            $query->withAggregate(['members as calculated_title' => $constraint], $titleColumn);
45 ✔
96
        } else {
NEW
97
            trigger_error('chat.classes.user.columns.full_name is not configured, calculated_title will be null.', E_USER_WARNING);
×
98
        }
99

100
        if ($avatarColumn = config('chat.classes.user.columns.avatar')) {
45 ✔
101
            $query->withAggregate(['members as calculated_cover_id' => $constraint], $avatarColumn);
45 ✔
102
        }
103

104
        return $query;
45 ✔
105
    }
106

107
    public function scopeWithUnreadMessagesCount(Builder $query, int $memberId): Builder
108
    {
109
        return $query->withCount([
14 ✔
110
            'messages as unread_messages_count' => fn ($query) => $query
14 ✔
111
                ->whereNot('sender_id', $memberId)
14 ✔
112
                ->whereDoesntHave('reads', fn ($query) => $query->where('read_messages.member_id', $memberId)),
14 ✔
113
        ]);
14 ✔
114
    }
115

116
    public function pinned_messages(): BelongsToMany
117
    {
118
        return $this
24 ✔
119
            ->belongsToMany(
24 ✔
120
                related: app()->getAlias(MessageModelContract::class),
24 ✔
121
                table: 'pinned_messages',
24 ✔
122
                foreignPivotKey: 'conversation_id',
24 ✔
123
                relatedPivotKey: 'message_id',
24 ✔
124
            )
24 ✔
125
            ->orderByPivot('id', 'desc')
24 ✔
126
            ->withTimestamps();
24 ✔
127
    }
128

129
    public function hasMember(Model $member): bool
130
    {
131
        return $this->members()->where('member_id', $member->id)->exists();
44 ✔
132
    }
133

134
    public function isCreator(Model $member): bool
135
    {
136
        return $this->getAttribute('creator_id') === $member->id;
4 ✔
137
    }
138

139
    public function isGroup(): bool
140
    {
141
        return $this->getAttribute('type') === TypeEnum::Group;
2 ✔
142
    }
143

144
    public function isPrivate(): bool
145
    {
146
        return $this->getAttribute('type') === TypeEnum::Private;
49 ✔
147
    }
148

149
    public function hasPinnedMessage(int $messageId): bool
150
    {
151
        return $this
5 ✔
152
            ->pinned_messages()
5 ✔
153
            ->whereKey($messageId)
5 ✔
154
            ->exists();
5 ✔
155
    }
156

157
    private function buildTitleExpression(array $columns): string
158
    {
159
        $expression = array_shift($columns);
48 ✔
160

161
        if (!empty($columns)) {
48 ✔
162
            $separators = config('chat.classes.user.columns.full_name_separator') ?: [' '];
47 ✔
163
            $separators = array_pad($separators, count($columns), end($separators));
47 ✔
164

165
            $expression = "COALESCE({$expression}, '')";
47 ✔
166

167
            foreach ($columns as $i => $column) {
47 ✔
168
                $sep = $this->getConnection()->escape($separators[$i]);
47 ✔
169
                $expression .= " || COALESCE({$sep} || {$column}, '')";
47 ✔
170
            }
171
        }
172

173
        return $expression;
48 ✔
174
    }
175
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc