614 lines
22 KiB
PHP
614 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Others\SocialBase\Livewire;
|
|
|
|
use Livewire\Component;
|
|
use Livewire\WithFileUploads;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Paymenter\Extensions\Others\SocialBase\Models\Conversation;
|
|
use Paymenter\Extensions\Others\SocialBase\Models\Message;
|
|
use Paymenter\Extensions\Others\SocialBase\Services\MessagingService;
|
|
use App\Models\User;
|
|
|
|
class MessagingInterface extends Component
|
|
{
|
|
use WithFileUploads;
|
|
public $conversations;
|
|
public $selectedConversation = null;
|
|
public $messages = [];
|
|
public $newMessage = '';
|
|
public $recipientId = null;
|
|
public $attachments = [];
|
|
|
|
public $showMembersModal = false;
|
|
public $showAddMembersModal = false;
|
|
public $showRenameModal = false;
|
|
public $groupMembers = [];
|
|
public $availableUsers = [];
|
|
public $selectedUsersToAdd = [];
|
|
public $newGroupName = '';
|
|
|
|
protected $listeners = [
|
|
'messageReceived' => 'loadMessages',
|
|
'messageSent' => 'refreshAll',
|
|
'groupCreated' => 'handleGroupCreated',
|
|
];
|
|
|
|
protected $rules = [
|
|
'attachments.*' => 'file|max:10240|mimes:jpg,jpeg,png,gif,webp,mp4,mov,avi,txt,php,js,css,html,json,xml,md,zip',
|
|
];
|
|
|
|
public function mount($conversationId = null, $recipientId = null)
|
|
{
|
|
$this->recipientId = $recipientId;
|
|
|
|
if ($conversationId) {
|
|
$this->selectConversation($conversationId);
|
|
} elseif ($recipientId) {
|
|
// Create or find conversation with recipient
|
|
$conversation = Conversation::findOrCreateDirectConversation(Auth::id(), $recipientId);
|
|
$this->selectConversation($conversation->id);
|
|
} else {
|
|
// Auto-select the most recent conversation
|
|
$this->loadConversations();
|
|
if (!empty($this->conversations) && is_array($this->conversations) && count($this->conversations) > 0) {
|
|
$firstConversation = reset($this->conversations);
|
|
if (isset($firstConversation['id'])) {
|
|
$this->selectConversation($firstConversation['id']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function loadConversations()
|
|
{
|
|
$service = new MessagingService();
|
|
$this->conversations = $service->getUserConversations(Auth::id())
|
|
->get()
|
|
->map(function ($conversation) {
|
|
$isPinned = $conversation->isPinnedForUser(Auth::id());
|
|
|
|
if ($conversation->type === 'group') {
|
|
// Group conversation
|
|
$participants = $conversation->participants()->get();
|
|
return [
|
|
'id' => $conversation->id,
|
|
'type' => $conversation->type,
|
|
'subject' => $conversation->subject,
|
|
'participants_count' => $participants->count(),
|
|
'participants' => $participants->take(3)->map(function ($user) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $user->id)->first();
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $profile?->display_name ?? $user->name,
|
|
'avatar' => $profile?->avatar_url ?? $user->avatar,
|
|
];
|
|
})->toArray(),
|
|
'other_user' => null,
|
|
'last_message' => $conversation->lastMessage,
|
|
'unread_count' => $conversation->getUnreadCountForUser(Auth::id()),
|
|
'last_message_at' => $conversation->last_message_at,
|
|
'created_by' => $conversation->created_by,
|
|
'is_pinned' => $isPinned,
|
|
];
|
|
} else {
|
|
// Direct conversation
|
|
$otherUser = $conversation->getOtherParticipant(Auth::id());
|
|
|
|
// Load SocialBase profile if available
|
|
$profile = null;
|
|
if ($otherUser) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $otherUser->id)->first();
|
|
}
|
|
|
|
return [
|
|
'id' => $conversation->id,
|
|
'type' => $conversation->type,
|
|
'subject' => $conversation->subject,
|
|
'other_user' => $otherUser ? [
|
|
'id' => $otherUser->id,
|
|
'name' => $profile ? $profile->display_name : $otherUser->name,
|
|
'avatar' => $profile ? $profile->avatar_url : $otherUser->avatar,
|
|
] : null,
|
|
'last_message' => $conversation->lastMessage,
|
|
'unread_count' => $conversation->getUnreadCountForUser(Auth::id()),
|
|
'last_message_at' => $conversation->last_message_at,
|
|
'created_by' => $conversation->created_by,
|
|
'is_pinned' => $isPinned,
|
|
];
|
|
}
|
|
})
|
|
->sort(function ($a, $b) {
|
|
// Sort: system first, then pinned, then by last message date
|
|
// System conversations always at the top
|
|
if ($a['type'] === 'system' && $b['type'] !== 'system') return -1;
|
|
if ($a['type'] !== 'system' && $b['type'] === 'system') return 1;
|
|
|
|
// Then by pinned status
|
|
if ($a['is_pinned'] && !$b['is_pinned']) return -1;
|
|
if (!$a['is_pinned'] && $b['is_pinned']) return 1;
|
|
|
|
// Finally by last message date
|
|
$timeA = $a['last_message_at'] ?? now()->subYears(10);
|
|
$timeB = $b['last_message_at'] ?? now()->subYears(10);
|
|
|
|
return $timeB <=> $timeA;
|
|
})
|
|
->values()
|
|
->toArray();
|
|
}
|
|
|
|
public function handleGroupCreated($conversationId)
|
|
{
|
|
$this->loadConversations();
|
|
$this->selectConversation($conversationId);
|
|
}
|
|
|
|
public function selectConversation($conversationId)
|
|
{
|
|
$conversation = Conversation::findOrFail($conversationId);
|
|
|
|
// Check if user is participant
|
|
if (!$conversation->hasParticipant(Auth::id())) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->selectedConversation = $conversationId;
|
|
$this->loadMessages();
|
|
|
|
// Mark as read
|
|
$conversation->markAsReadForUser(Auth::id());
|
|
}
|
|
|
|
public function loadMessages()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
$this->messages = $conversation->messages()
|
|
->with(['sender', 'reactions.user'])
|
|
->get()
|
|
->map(function ($message) {
|
|
// Load SocialBase profile if available
|
|
$profile = null;
|
|
if ($message->sender) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $message->sender_id)->first();
|
|
}
|
|
|
|
// Group reactions by type and get counts
|
|
$reactionCounts = $message->reactions->groupBy('type')->map(function ($reactions, $type) {
|
|
return [
|
|
'emoji' => \Paymenter\Extensions\Others\SocialBase\Models\Reaction::TYPES[$type] ?? $type,
|
|
'count' => $reactions->count(),
|
|
'users' => $reactions->pluck('user.name')->toArray(),
|
|
'hasCurrentUser' => $reactions->contains('user_id', Auth::id()),
|
|
];
|
|
})->toArray();
|
|
|
|
return [
|
|
'id' => $message->id,
|
|
'content' => $message->content,
|
|
'sender_id' => $message->sender_id,
|
|
'sender_name' => $message->sender ? ($profile ? $profile->display_name : $message->sender->name) : 'System',
|
|
'sender_avatar' => $message->sender ? ($profile ? $profile->avatar_url : $message->sender->avatar) : null,
|
|
'is_system' => $message->is_system,
|
|
'system_type' => $message->system_type,
|
|
'created_at' => $message->created_at,
|
|
'is_mine' => $message->sender_id === Auth::id(),
|
|
'reactions' => $reactionCounts,
|
|
'attachments' => $message->attachments ?? [],
|
|
];
|
|
})
|
|
->toArray();
|
|
|
|
// Dispatch event to trigger scroll after messages are loaded
|
|
$this->dispatch('messagesLoaded');
|
|
}
|
|
|
|
public function sendMessage()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
// Block sending messages to system conversations
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
if (!$conversation || $conversation->type === 'system') {
|
|
session()->flash('error', 'You cannot send messages to system conversations.');
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'newMessage' => 'required_without:attachments|string|max:2000',
|
|
'attachments.*' => 'file|max:10240|mimes:jpg,jpeg,png,gif,webp,mp4,mov,avi,txt,php,js,css,html,json,xml,md,zip',
|
|
]);
|
|
|
|
$uploadedFiles = [];
|
|
if (!empty($this->attachments)) {
|
|
foreach ($this->attachments as $attachment) {
|
|
// Store in private storage for security
|
|
$path = $attachment->store('socialbase/attachments', 'local');
|
|
$uploadedFiles[] = [
|
|
'path' => $path,
|
|
'name' => $attachment->getClientOriginalName(),
|
|
'type' => $attachment->getMimeType(),
|
|
'size' => $attachment->getSize(),
|
|
'extension' => $attachment->getClientOriginalExtension(),
|
|
];
|
|
}
|
|
}
|
|
|
|
$message = Message::create([
|
|
'conversation_id' => $this->selectedConversation,
|
|
'sender_id' => Auth::id(),
|
|
'content' => $this->newMessage ?? '',
|
|
'attachments' => !empty($uploadedFiles) ? $uploadedFiles : null,
|
|
'is_system' => false,
|
|
]);
|
|
|
|
// Update conversation's last message timestamp
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
if ($conversation) {
|
|
$conversation->last_message_at = now();
|
|
$conversation->save();
|
|
}
|
|
|
|
$this->newMessage = '';
|
|
$this->attachments = [];
|
|
$this->loadMessages();
|
|
$this->loadConversations();
|
|
|
|
// Mark as read immediately after sending
|
|
$conversation->markAsReadForUser(Auth::id());
|
|
|
|
// Dispatch event for real-time updates
|
|
$this->dispatch('messageSent');
|
|
}
|
|
|
|
public function removeAttachment($index)
|
|
{
|
|
unset($this->attachments[$index]);
|
|
$this->attachments = array_values($this->attachments);
|
|
}
|
|
|
|
public function togglePin($conversationId)
|
|
{
|
|
$conversation = Conversation::findOrFail($conversationId);
|
|
|
|
// Prevent unpinning system conversations
|
|
if ($conversation->type === 'system') {
|
|
return;
|
|
}
|
|
|
|
// Check if user is participant
|
|
if (!$conversation->hasParticipant(Auth::id())) {
|
|
return;
|
|
}
|
|
|
|
$conversation->togglePinForUser(Auth::id());
|
|
$this->loadConversations();
|
|
}
|
|
|
|
public function refreshAll()
|
|
{
|
|
$this->loadConversations();
|
|
if ($this->selectedConversation) {
|
|
$this->loadMessages();
|
|
|
|
// Mark as read when refreshing an open conversation
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
if ($conversation) {
|
|
$conversation->markAsReadForUser(Auth::id());
|
|
}
|
|
}
|
|
}
|
|
|
|
public function deleteMessage($messageId)
|
|
{
|
|
$message = Message::find($messageId);
|
|
|
|
if (!$message) {
|
|
$this->dispatch('error', 'Message not found.');
|
|
return;
|
|
}
|
|
|
|
// Only allow user to delete their own messages
|
|
if ($message->sender_id !== Auth::id()) {
|
|
$this->dispatch('error', 'You can only delete your own messages.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$message->delete();
|
|
$this->loadMessages();
|
|
$this->dispatch('success', 'Message deleted successfully.');
|
|
} catch (\Exception $e) {
|
|
$this->dispatch('error', 'Failed to delete message.');
|
|
}
|
|
}
|
|
|
|
public function reactToMessage($messageId, $reaction)
|
|
{
|
|
$message = Message::find($messageId);
|
|
|
|
if (!$message) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$message->toggleReaction(Auth::user(), $reaction);
|
|
$this->loadMessages();
|
|
} catch (\Exception $e) {
|
|
// Fail silently
|
|
}
|
|
}
|
|
|
|
// Group Management Methods
|
|
public function openViewMembersModal()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group') {
|
|
return;
|
|
}
|
|
|
|
$this->groupMembers = $conversation->participants()->get()->map(function ($user) use ($conversation) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $user->id)->first();
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $profile ? $profile->display_name : $user->name,
|
|
'email' => $user->email,
|
|
'avatar' => $profile ? $profile->avatar_url : $user->avatar,
|
|
'is_creator' => $user->id == $conversation->created_by,
|
|
];
|
|
})->toArray();
|
|
|
|
$this->showMembersModal = true;
|
|
}
|
|
|
|
public function closeViewMembersModal()
|
|
{
|
|
$this->showMembersModal = false;
|
|
$this->groupMembers = [];
|
|
}
|
|
|
|
public function openAddMembersModal()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group' || $conversation->created_by != Auth::id()) {
|
|
return;
|
|
}
|
|
|
|
$existingMemberIds = $conversation->participants()->pluck('users.id')->toArray();
|
|
|
|
$this->availableUsers = User::where('id', '!=', Auth::id())
|
|
->whereNotIn('id', $existingMemberIds)
|
|
->get()
|
|
->map(function ($user) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $user->id)->first();
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $profile ? $profile->display_name : $user->name,
|
|
'avatar' => $profile ? $profile->avatar_url : $user->avatar,
|
|
];
|
|
})
|
|
->toArray();
|
|
|
|
$this->selectedUsersToAdd = [];
|
|
$this->showAddMembersModal = true;
|
|
}
|
|
|
|
public function closeAddMembersModal()
|
|
{
|
|
$this->showAddMembersModal = false;
|
|
$this->availableUsers = [];
|
|
$this->selectedUsersToAdd = [];
|
|
}
|
|
|
|
public function toggleUserToAdd($userId)
|
|
{
|
|
if (in_array($userId, $this->selectedUsersToAdd)) {
|
|
$this->selectedUsersToAdd = array_values(array_diff($this->selectedUsersToAdd, [$userId]));
|
|
} else {
|
|
$this->selectedUsersToAdd[] = $userId;
|
|
}
|
|
}
|
|
|
|
public function addMembersToGroup()
|
|
{
|
|
if (empty($this->selectedUsersToAdd)) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group' || $conversation->created_by != Auth::id()) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->selectedUsersToAdd as $userId) {
|
|
$conversation->participants()->attach($userId);
|
|
|
|
// Send system message
|
|
$user = User::find($userId);
|
|
if ($user) {
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $userId)->first();
|
|
$displayName = $profile ? $profile->display_name : $user->name;
|
|
|
|
Message::create([
|
|
'conversation_id' => $conversation->id,
|
|
'sender_id' => null,
|
|
'content' => "**Member Added**\n\n" . Auth::user()->name . " added " . $displayName . " to the group",
|
|
'type' => 'system',
|
|
'is_system' => true,
|
|
'system_type' => 'member_added',
|
|
'metadata' => [],
|
|
]);
|
|
}
|
|
}
|
|
|
|
$conversation->update(['last_message_at' => now()]);
|
|
|
|
$this->closeAddMembersModal();
|
|
$this->loadConversations();
|
|
$this->loadMessages();
|
|
}
|
|
|
|
public function openRenameModal()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group' || $conversation->created_by != Auth::id()) {
|
|
return;
|
|
}
|
|
|
|
$this->newGroupName = $conversation->subject;
|
|
$this->showRenameModal = true;
|
|
}
|
|
|
|
public function closeRenameModal()
|
|
{
|
|
$this->showRenameModal = false;
|
|
$this->newGroupName = '';
|
|
}
|
|
|
|
public function renameGroup()
|
|
{
|
|
$this->validate([
|
|
'newGroupName' => 'required|string|max:255',
|
|
]);
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group' || $conversation->created_by != Auth::id()) {
|
|
return;
|
|
}
|
|
|
|
$oldName = $conversation->subject;
|
|
$conversation->update(['subject' => $this->newGroupName]);
|
|
|
|
// Send system message
|
|
Message::create([
|
|
'conversation_id' => $conversation->id,
|
|
'sender_id' => null,
|
|
'content' => "**Group Renamed**\n\n" . Auth::user()->name . " renamed the group from \"" . $oldName . "\" to \"" . $this->newGroupName . "\"",
|
|
'type' => 'system',
|
|
'is_system' => true,
|
|
'system_type' => 'group_renamed',
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$conversation->update(['last_message_at' => now()]);
|
|
|
|
$this->closeRenameModal();
|
|
$this->loadConversations();
|
|
$this->loadMessages();
|
|
}
|
|
|
|
public function leaveGroup()
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group') {
|
|
return;
|
|
}
|
|
|
|
// Send system message before leaving
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', Auth::id())->first();
|
|
$displayName = $profile ? $profile->display_name : Auth::user()->name;
|
|
|
|
Message::create([
|
|
'conversation_id' => $conversation->id,
|
|
'sender_id' => null,
|
|
'content' => "**Member Left**\n\n" . $displayName . " left the group",
|
|
'type' => 'system',
|
|
'is_system' => true,
|
|
'system_type' => 'member_left',
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$conversation->update(['last_message_at' => now()]);
|
|
|
|
// Remove user from conversation
|
|
$conversation->participants()->detach(Auth::id());
|
|
|
|
// Reset selection
|
|
$this->selectedConversation = null;
|
|
$this->messages = [];
|
|
$this->loadConversations();
|
|
}
|
|
|
|
public function removeMember($userId)
|
|
{
|
|
if (!$this->selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
$conversation = Conversation::find($this->selectedConversation);
|
|
|
|
if (!$conversation || $conversation->type !== 'group' || $conversation->created_by != Auth::id()) {
|
|
return;
|
|
}
|
|
|
|
// Can't remove creator
|
|
if ($userId == $conversation->created_by) {
|
|
return;
|
|
}
|
|
|
|
$user = User::find($userId);
|
|
if (!$user) {
|
|
return;
|
|
}
|
|
|
|
$profile = \Paymenter\Extensions\Others\SocialBase\Models\UserProfile::where('user_id', $userId)->first();
|
|
$displayName = $profile ? $profile->display_name : $user->name;
|
|
|
|
// Send system message
|
|
Message::create([
|
|
'conversation_id' => $conversation->id,
|
|
'sender_id' => null,
|
|
'content' => "**Member Removed**\n\n" . Auth::user()->name . " removed " . $displayName . " from the group",
|
|
'type' => 'system',
|
|
'is_system' => true,
|
|
'system_type' => 'member_removed',
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$conversation->update(['last_message_at' => now()]);
|
|
|
|
// Remove user from conversation
|
|
$conversation->participants()->detach($userId);
|
|
|
|
// Refresh members list
|
|
$this->openViewMembersModal();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$this->loadConversations();
|
|
|
|
return view('socialbase::livewire.messaging-interface');
|
|
}
|
|
}
|
|
|