Files
Virtnet.bond/extensions/Others/SocialBase/Livewire/MessagingInterface.php
2025-11-30 11:57:22 +01:00

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');
}
}