1017 lines
77 KiB
PHP
1017 lines
77 KiB
PHP
<div class="messaging-bubble-container"
|
|
x-data="{
|
|
pollingInterval: null,
|
|
lastMessageCount: 0,
|
|
bubbleWidth: parseInt(localStorage.getItem('bubbleWidth')) || 400,
|
|
bubbleHeight: parseInt(localStorage.getItem('bubbleHeight')) || 600,
|
|
isResizing: false,
|
|
startX: 0,
|
|
startY: 0,
|
|
startWidth: 0,
|
|
startHeight: 0,
|
|
startPolling() {
|
|
this.pollingInterval = setInterval(() => {
|
|
@this.call('refresh');
|
|
}, 5000); // Poll every 5 seconds
|
|
},
|
|
stopPolling() {
|
|
if (this.pollingInterval) {
|
|
clearInterval(this.pollingInterval);
|
|
}
|
|
},
|
|
playNotificationSound() {
|
|
// Modern notification sound (pleasant pop sound)
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
// Create a pleasant notification tone
|
|
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
|
oscillator.frequency.exponentialRampToValueAtTime(400, audioContext.currentTime + 0.1);
|
|
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
|
|
|
oscillator.start(audioContext.currentTime);
|
|
oscillator.stop(audioContext.currentTime + 0.3);
|
|
},
|
|
startResize(event, direction) {
|
|
event.preventDefault();
|
|
this.isResizing = true;
|
|
this.direction = direction;
|
|
this.startX = event.clientX;
|
|
this.startY = event.clientY;
|
|
this.startWidth = parseInt(this.bubbleWidth);
|
|
this.startHeight = parseInt(this.bubbleHeight);
|
|
|
|
const self = this;
|
|
const onMouseMove = (moveEvent) => {
|
|
if (!self.isResizing) return;
|
|
|
|
if (direction.includes('left')) {
|
|
const deltaX = self.startX - moveEvent.clientX;
|
|
self.bubbleWidth = Math.max(300, Math.min(800, self.startWidth + deltaX));
|
|
}
|
|
if (direction.includes('top')) {
|
|
const deltaY = self.startY - moveEvent.clientY;
|
|
self.bubbleHeight = Math.max(400, Math.min(900, self.startHeight + deltaY));
|
|
}
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
self.isResizing = false;
|
|
localStorage.setItem('bubbleWidth', self.bubbleWidth);
|
|
localStorage.setItem('bubbleHeight', self.bubbleHeight);
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
};
|
|
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
}
|
|
}"
|
|
x-init="startPolling();
|
|
$watch('$wire.unreadCount', (value, oldValue) => {
|
|
if (value > oldValue && oldValue > 0) {
|
|
playNotificationSound();
|
|
}
|
|
})"
|
|
x-on:beforeunload.window="stopPolling()"
|
|
x-show="!window.location.pathname.includes('/messages')"
|
|
style="display: block;">
|
|
|
|
@include('socialbase::components.scrollbar-styles')
|
|
|
|
@if($isOpen)
|
|
<!-- Messaging Window -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border-2 border-gray-200 dark:border-gray-600 overflow-hidden"
|
|
:style="`position: fixed; bottom: 80px; right: 16px; width: ${bubbleWidth}px; height: ${bubbleHeight}px; z-index: 9998; display: flex; flex-direction: column;`"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 transform scale-95"
|
|
x-transition:enter-end="opacity-100 transform scale-100">
|
|
|
|
<!-- Resize Handles -->
|
|
<!-- Top-left corner -->
|
|
<div
|
|
@mousedown="startResize($event, 'top-left')"
|
|
class="absolute top-0 left-0 w-6 h-6 z-50 group"
|
|
style="touch-action: none; cursor: nwse-resize;"
|
|
>
|
|
<div class="absolute top-1 left-1 w-3 h-3 border-t-2 border-l-2 border-blue-500 opacity-0 group-hover:opacity-100 transition-opacity rounded-tl"></div>
|
|
</div>
|
|
|
|
<!-- Top edge -->
|
|
<div
|
|
@mousedown="startResize($event, 'top')"
|
|
class="absolute top-0 left-6 right-0 h-3 z-50"
|
|
style="touch-action: none; cursor: ns-resize;"
|
|
></div>
|
|
|
|
<!-- Left edge -->
|
|
<div
|
|
@mousedown="startResize($event, 'left')"
|
|
class="absolute top-6 bottom-0 left-0 w-3 z-50"
|
|
style="touch-action: none; cursor: ew-resize;"
|
|
></div>
|
|
|
|
<!-- Header -->
|
|
<div class="bg-white dark:bg-gray-800 px-4 py-4 border-b-2 border-gray-200 dark:border-gray-600 flex items-center justify-between flex-shrink-0">
|
|
<div class="flex items-center gap-2">
|
|
@if($selectedConversation)
|
|
<button wire:click="backToList" class="hover:bg-gray-100 dark:hover:bg-gray-700 p-1.5 rounded transition-colors" title="Back">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-gray-700 dark:text-gray-300">
|
|
<path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z"></path>
|
|
</svg>
|
|
</button>
|
|
@endif
|
|
<i class="ri-message-3-line text-xl text-gray-700 dark:text-gray-300"></i>
|
|
<h3 class="font-semibold text-lg text-gray-900 dark:text-white">Messages</h3>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
@if(!$selectedConversation)
|
|
<!--<button
|
|
@click="$dispatch('open-create-group')"
|
|
class="px-2.5 py-1.5 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg font-medium shadow-md hover:shadow-lg transition-all text-xs flex items-center gap-1.5"
|
|
title="Create Group Chat">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
<span>New Group</span>
|
|
</button>-->
|
|
@endif
|
|
<button wire:click="toggleBubble" class="hover:bg-gray-100 dark:hover:bg-gray-700 p-1.5 rounded transition-colors" title="Close">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-gray-700 dark:text-gray-300">
|
|
<path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(!$selectedConversation)
|
|
<!-- Conversations List -->
|
|
<div class="flex-1 overflow-y-auto bg-gray-50 dark:bg-gray-900 socialbase-scroll flex flex-col">
|
|
<div class="flex-1 overflow-y-auto socialbase-scroll">
|
|
@forelse($recentConversations as $conversation)
|
|
<div wire:click="selectConversation({{ $conversation['id'] }})"
|
|
class="p-4 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer transition-colors group relative">
|
|
<!-- Pin Button (hidden for system conversations) -->
|
|
@if($conversation['type'] !== 'system')
|
|
<button
|
|
wire:click.stop="togglePin({{ $conversation['id'] }})"
|
|
class="absolute top-2 right-2 p-1 rounded transition-colors {{ $conversation['is_pinned'] ? 'text-yellow-500 hover:text-yellow-600' : 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 opacity-0 group-hover:opacity-100' }}"
|
|
title="{{ $conversation['is_pinned'] ? 'Unpin conversation' : 'Pin conversation' }}"
|
|
>
|
|
<svg class="w-3.5 h-3.5 {{ $conversation['is_pinned'] ? 'fill-current' : '' }}" fill="{{ $conversation['is_pinned'] ? 'currentColor' : 'none' }}" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
|
|
</svg>
|
|
</button>
|
|
@endif
|
|
|
|
<div class="flex items-center gap-3">
|
|
@if($conversation['type'] === 'direct' && $conversation['other_user'])
|
|
<img src="{{ $conversation['other_user']['avatar'] }}"
|
|
alt="{{ $conversation['other_user']['name'] }}"
|
|
class="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-800">
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-gray-900 dark:text-white truncate mb-1">
|
|
{{ $conversation['other_user']['name'] }}
|
|
</h4>
|
|
@if($conversation['last_message'])
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">
|
|
{{ \Illuminate\Support\Str::limit(strip_tags(preg_replace('/\*\*|\*|__|_|~~|`|```|\|\||\[([^\]]+)\]\([^\)]+\)/', '$1', $conversation['last_message']->content)), 50) }}
|
|
</p>
|
|
@endif
|
|
@if($conversation['last_message_at'])
|
|
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
|
{{ $conversation['last_message_at']->diffForHumans() }}
|
|
</p>
|
|
@endif
|
|
</div>
|
|
@if($conversation['unread_count'] > 0)
|
|
<span class="bg-blue-500 text-white text-xs px-2 py-1 rounded-full flex items-center justify-center flex-shrink-0 font-medium">
|
|
{{ $conversation['unread_count'] }}
|
|
</span>
|
|
@endif
|
|
@elseif($conversation['type'] === 'group')
|
|
<!-- Group Avatar -->
|
|
@if(isset($conversation['participants']) && count($conversation['participants']) > 0)
|
|
<div class="flex -space-x-2 flex-shrink-0">
|
|
@foreach(array_slice($conversation['participants'], 0, 2) as $participant)
|
|
@if($participant['avatar'])
|
|
<img src="{{ $participant['avatar'] }}"
|
|
alt="{{ $participant['name'] }}"
|
|
class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 object-cover" />
|
|
@else
|
|
<div class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center text-white text-xs font-bold">
|
|
{{ strtoupper(substr($participant['name'], 0, 1)) }}
|
|
</div>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center flex-shrink-0">
|
|
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
|
</svg>
|
|
</div>
|
|
@endif
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-gray-900 dark:text-white truncate mb-1 flex items-center gap-2">
|
|
{{ $conversation['subject'] ?? 'Group Chat' }}
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 font-normal">
|
|
({{ $conversation['participants_count'] ?? 0 }})
|
|
</span>
|
|
</h4>
|
|
@if($conversation['last_message'])
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">
|
|
{{ \Illuminate\Support\Str::limit(strip_tags(preg_replace('/\*\*|\*|__|_|~~|`|```|\|\||\[([^\]]+)\]\([^\)]+\)/', '$1', $conversation['last_message']->content)), 50) }}
|
|
</p>
|
|
@endif
|
|
@if($conversation['last_message_at'])
|
|
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
|
{{ $conversation['last_message_at']->diffForHumans() }}
|
|
</p>
|
|
@endif
|
|
</div>
|
|
@if($conversation['unread_count'] > 0)
|
|
<span class="bg-blue-500 text-white text-xs px-2 py-1 rounded-full flex items-center justify-center flex-shrink-0 font-medium">
|
|
{{ $conversation['unread_count'] }}
|
|
</span>
|
|
@endif
|
|
@elseif($conversation['type'] === 'system')
|
|
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
|
|
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM11 15V17H13V15H11ZM11 7V13H13V7H11Z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-gray-900 dark:text-white truncate mb-1">System Messages</h4>
|
|
@if($conversation['last_message'])
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">
|
|
{{ \Illuminate\Support\Str::limit(strip_tags(preg_replace('/\*\*|\*|__|_|~~|`|```|\|\||\[([^\]]+)\]\([^\)]+\)/', '$1', $conversation['last_message']->content)), 50) }}
|
|
</p>
|
|
@endif
|
|
</div>
|
|
@if($conversation['unread_count'] > 0)
|
|
<span class="bg-blue-500 text-white text-xs px-2 py-1 rounded-full flex items-center justify-center flex-shrink-0 font-medium">
|
|
{{ $conversation['unread_count'] }}
|
|
</span>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-12 h-12 mx-auto mb-2 opacity-30">
|
|
<path d="M7.29117 20.8242L2 22L3.17581 16.7088C2.42544 15.3056 2 13.7025 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C10.2975 22 8.6944 21.5746 7.29117 20.8242Z"></path>
|
|
</svg>
|
|
<p class="text-sm font-medium">No messages yet</p>
|
|
<p class="text-xs mt-1">Start a conversation to see it here</p>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<!-- Sticky Footer -->
|
|
<a href="{{ route('socialbase.messages.index') }}"
|
|
class="flex-shrink-0 block p-4 text-center text-blue-500 dark:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors flex items-center justify-center gap-2 border-t border-gray-200 dark:border-gray-700 font-medium bg-white dark:bg-gray-900">
|
|
<span>View All Messages</span>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
<path d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z"></path>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
@else
|
|
<!-- Message Thread -->
|
|
<div class="flex-1 flex flex-col overflow-hidden">
|
|
<!-- Chat Header -->
|
|
@php
|
|
$currentConv = collect($recentConversations)->firstWhere('id', $selectedConversation);
|
|
$otherUser = $currentConv['other_user'] ?? null;
|
|
@endphp
|
|
@if($otherUser)
|
|
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex items-center justify-between">
|
|
<a href="{{ route('socialbase.profile.show', $otherUser['id']) }}"
|
|
class="flex items-center gap-2 hover:opacity-80 transition-opacity flex-1"
|
|
wire:click="toggleBubble">
|
|
<img
|
|
src="{{ $otherUser['avatar'] }}"
|
|
alt="{{ $otherUser['name'] }}"
|
|
class="w-8 h-8 rounded-full object-cover"
|
|
>
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-sm text-gray-900 dark:text-white truncate">{{ $otherUser['name'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Tap to view profile</p>
|
|
</div>
|
|
</a>
|
|
|
|
<div class="flex items-center gap-1">
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button
|
|
@click.stop="open = !open"
|
|
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-600 dark:text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="1"/>
|
|
<circle cx="19" cy="12" r="1"/>
|
|
<circle cx="5" cy="12" r="1"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
@click.away="open = false"
|
|
x-transition
|
|
class="absolute right-0 mt-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50"
|
|
>
|
|
<a
|
|
href="{{ route('socialbase.profile.show', $otherUser['id']) }}"
|
|
wire:click="toggleBubble"
|
|
class="block px-3 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg"
|
|
>
|
|
<i class="ri-user-line mr-1.5"></i>View Profile
|
|
</a>
|
|
<button
|
|
@click="$dispatch('open-report-modal'); userDropdownOpen = false"
|
|
class="w-full text-left px-3 py-2 text-xs text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg"
|
|
>
|
|
<i class="ri-flag-line mr-1.5"></i>Report User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
wire:click="backToList"
|
|
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
|
|
title="Back to conversations"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-gray-600 dark:text-gray-400">
|
|
<path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@elseif($currentConv && $currentConv['type'] === 'group')
|
|
<!-- Group Chat Header -->
|
|
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 flex items-center justify-between">
|
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
|
<!-- Group Avatar -->
|
|
@if(count($currentConv['participants']) > 0)
|
|
<div class="flex -space-x-1 flex-shrink-0">
|
|
@foreach(array_slice($currentConv['participants'], 0, 2) as $participant)
|
|
@if($participant['avatar'])
|
|
<img src="{{ $participant['avatar'] }}"
|
|
alt="{{ $participant['name'] }}"
|
|
class="w-6 h-6 rounded-full border border-white dark:border-gray-800 object-cover" />
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-sm text-gray-900 dark:text-white truncate">
|
|
{{ $currentConv['subject'] ?? 'Group Chat' }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ $currentConv['participants_count'] ?? count($currentConv['participants'] ?? []) }} members
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1">
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button
|
|
@click.stop="open = !open"
|
|
class="p-1.5 hover:bg-purple-100 dark:hover:bg-purple-900/30 rounded transition-colors"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-600 dark:text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="1"/>
|
|
<circle cx="19" cy="12" r="1"/>
|
|
<circle cx="5" cy="12" r="1"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
@click.away="open = false"
|
|
x-transition
|
|
class="absolute right-0 mt-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50"
|
|
>
|
|
<button
|
|
wire:click="openViewMembersModal"
|
|
@click="open = false"
|
|
class="w-full text-left px-3 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg"
|
|
>
|
|
<i class="ri-team-line mr-1.5"></i>View Members
|
|
</button>
|
|
@if($currentConv['is_creator'] ?? false)
|
|
<button
|
|
wire:click="openAddMembersModal"
|
|
@click="open = false"
|
|
class="w-full text-left px-3 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
<i class="ri-user-add-line mr-1.5"></i>Add Members
|
|
</button>
|
|
<button
|
|
wire:click="openRenameModal"
|
|
@click="open = false"
|
|
class="w-full text-left px-3 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
<i class="ri-edit-line mr-1.5"></i>Rename Group
|
|
</button>
|
|
@endif
|
|
<button
|
|
wire:click="leaveGroup"
|
|
wire:confirm="Are you sure you want to leave this group?"
|
|
@click="open = false"
|
|
class="w-full text-left px-3 py-2 text-xs text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg"
|
|
>
|
|
<i class="ri-logout-box-line mr-1.5"></i>Leave Group
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
wire:click="backToList"
|
|
class="p-1.5 hover:bg-purple-100 dark:hover:bg-purple-900/30 rounded transition-colors"
|
|
title="Back to conversations"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-purple-600 dark:text-purple-400">
|
|
<path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Messages -->
|
|
<div class="flex-1 overflow-y-auto overflow-x-hidden socialbase-scroll p-3 space-y-2 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800" id="bubble-messages">
|
|
@forelse($messages as $message)
|
|
<div class="flex {{ $message['is_mine'] ? 'justify-end' : 'justify-start' }}">
|
|
<div class="flex gap-2 max-w-[75%] {{ $message['is_mine'] ? 'flex-row-reverse' : '' }}">
|
|
@if(!$message['is_system'] && !$message['is_mine'])
|
|
<img src="{{ $message['sender_avatar'] }}"
|
|
alt="{{ $message['sender_name'] }}"
|
|
class="w-6 h-6 rounded-full object-cover flex-shrink-0">
|
|
@endif
|
|
<div>
|
|
@if($message['is_system'])
|
|
<div class="flex items-start gap-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 rounded-lg p-2">
|
|
<div class="flex-shrink-0">
|
|
<div class="p-1 bg-blue-500/20 rounded">
|
|
<svg class="w-3.5 h-3.5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-[10px] font-semibold text-blue-700 dark:text-blue-300 mb-0.5">SYSTEM</p>
|
|
<div class="text-xs text-gray-800 dark:text-gray-200 prose prose-xs dark:prose-invert max-w-none">
|
|
{!! \Paymenter\Extensions\Others\SocialBase\Helpers\MessageFormatter::format($message['content']) !!}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="group relative">
|
|
<div class="{{ $message['is_mine'] ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white' }} rounded-lg p-2">
|
|
@if(!empty($message['content']))
|
|
<div class="text-xs break-words prose prose-xs {{ $message['is_mine'] ? 'prose-invert' : 'dark:prose-invert' }} max-w-none">
|
|
{!! \Paymenter\Extensions\Others\SocialBase\Helpers\MessageFormatter::format($message['content']) !!}
|
|
</div>
|
|
@endif
|
|
|
|
@if(!empty($message['attachments']))
|
|
<div class="mt-1.5 flex flex-wrap gap-1.5">
|
|
@foreach($message['attachments'] as $index => $attachment)
|
|
@php
|
|
$isImage = in_array(strtolower($attachment['extension'] ?? ''), ['jpg', 'jpeg', 'png', 'gif', 'webp']);
|
|
$isVideo = in_array(strtolower($attachment['extension'] ?? ''), ['mp4', 'mov', 'avi']);
|
|
$secureUrl = route('socialbase.messages.attachment.download', ['messageId' => $message['id'], 'attachmentIndex' => $index]);
|
|
@endphp
|
|
|
|
@if($isImage)
|
|
<a href="{{ $secureUrl }}" target="_blank" class="block">
|
|
<img src="{{ $secureUrl }}" alt="{{ $attachment['name'] ?? 'Image' }}" class="max-w-[180px] max-h-32 rounded {{ $message['is_mine'] ? 'border border-white/30' : 'border border-gray-300 dark:border-gray-600' }} hover:opacity-90 transition-opacity">
|
|
</a>
|
|
@elseif($isVideo)
|
|
<video controls class="max-w-[180px] max-h-32 rounded {{ $message['is_mine'] ? 'border border-white/30' : 'border border-gray-300 dark:border-gray-600' }}">
|
|
<source src="{{ $secureUrl }}" type="{{ $attachment['type'] ?? 'video/mp4' }}">
|
|
</video>
|
|
@else
|
|
<a href="{{ $secureUrl }}" download="{{ $attachment['name'] ?? 'file' }}" class="flex items-center gap-1.5 px-2 py-1 text-xs {{ $message['is_mine'] ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500' }} rounded transition-colors">
|
|
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
|
</svg>
|
|
<span class="truncate max-w-[120px]">{{ $attachment['name'] ?? 'file' }}</span>
|
|
</a>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Message Actions Dropdown -->
|
|
@if(!$message['is_system'])
|
|
<div class="absolute {{ $message['is_mine'] ? '-left-10' : '-right-10' }} top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" x-data="{ open: false, showReactions: false }">
|
|
<button @click="open = !open" class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-1.5 py-0.5 shadow-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
|
<svg class="w-3 h-3 text-gray-600 dark:text-gray-300" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<div x-show="open"
|
|
@click.away="open = false"
|
|
x-transition:enter="transition ease-out duration-100"
|
|
x-transition:enter-start="transform opacity-0 scale-95"
|
|
x-transition:enter-end="transform opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75"
|
|
x-transition:leave-start="transform opacity-100 scale-100"
|
|
x-transition:leave-end="transform opacity-0 scale-95"
|
|
class="absolute {{ $message['is_mine'] ? 'right-full mr-1' : 'left-full ml-1' }} top-0 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 z-50"
|
|
style="display: none;">
|
|
@if(!$message['is_mine'])
|
|
<button @click="showReactions = !showReactions; open = false" class="w-full text-left px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center rounded-t-lg transition-colors">
|
|
<span class="text-base mr-1.5">😊</span>
|
|
<span>React</span>
|
|
</button>
|
|
<button @click="open = false; $dispatch('open-report-modal')" class="w-full text-left px-3 py-1.5 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center transition-colors">
|
|
<svg class="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9"></path>
|
|
</svg>
|
|
<span>Report</span>
|
|
</button>
|
|
@else
|
|
<button wire:click="deleteMessage({{ $message['id'] }})" @click="open = false" class="w-full text-left px-3 py-1.5 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center rounded-lg transition-colors">
|
|
<svg class="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
<span>Delete</span>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Reaction Picker (Inside x-data scope) -->
|
|
@if(!$message['is_mine'])
|
|
<div x-show="showReactions"
|
|
@click.away="showReactions = false"
|
|
class="absolute {{ $message['is_mine'] ? 'right-0 mr-10' : 'left-0 ml-10' }} top-0 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 p-2 z-[60]"
|
|
style="display: none; min-width: 220px;">
|
|
<div class="grid grid-cols-5 gap-1.5">
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'like')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Like">👍</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'love')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Love">❤️</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'laugh')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Laugh">😂</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'wow')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Wow">😮</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'sad')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Sad">😢</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'angry')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Angry">😠</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'celebrate')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Celebrate">🎉</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'thinking')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Thinking">🤔</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'clap')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Clap">👏</button>
|
|
<button wire:click="reactToMessage({{ $message['id'] }}, 'fire')" @click="showReactions = false" class="text-2xl hover:scale-125 transition-transform p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Fire">🔥</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Reactions Display -->
|
|
@if(isset($message['reactions']) && count($message['reactions']) > 0)
|
|
<div class="flex flex-wrap gap-1 mt-2">
|
|
@foreach($message['reactions'] as $type => $reaction)
|
|
<button
|
|
wire:click="reactToMessage({{ $message['id'] }}, '{{ $type }}')"
|
|
class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs transition-all {{ $reaction['hasCurrentUser'] ? 'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500' : 'bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600' }}"
|
|
title="{{ implode(', ', $reaction['users']) }}">
|
|
<span class="text-sm">{{ $reaction['emoji'] }}</span>
|
|
<span class="font-medium {{ $reaction['hasCurrentUser'] ? 'text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-gray-300' }}">{{ $reaction['count'] }}</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1 {{ $message['is_mine'] ? 'text-right' : '' }}">
|
|
{{ $message['created_at']->format('g:i A') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10 mx-auto mb-2 opacity-50">
|
|
<path d="M6.45455 19L2 22.5V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.45455Z"></path>
|
|
</svg>
|
|
<p class="text-xs">No messages yet</p>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<!-- Message Input -->
|
|
@php
|
|
$currentConv = collect($recentConversations)->firstWhere('id', $selectedConversation);
|
|
$isSystemConversation = $currentConv && $currentConv['type'] === 'system';
|
|
@endphp
|
|
|
|
<div class="border-t border-gray-200 dark:border-gray-700 p-2 flex-shrink-0">
|
|
@if($isSystemConversation)
|
|
<div class="text-center py-2 px-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 flex items-center justify-center gap-2">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
|
</svg>
|
|
<span>Read-only conversation</span>
|
|
</p>
|
|
</div>
|
|
@else
|
|
<!-- Attachment Previews -->
|
|
@if(!empty($attachments))
|
|
<div class="mb-2" wire:loading.remove wire:target="attachments">
|
|
<div class="flex flex-wrap gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
|
@foreach($attachments as $index => $attachment)
|
|
@php
|
|
$extension = strtolower($attachment->getClientOriginalExtension());
|
|
$isImage = in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp']);
|
|
$isVideo = in_array($extension, ['mp4', 'mov', 'avi']);
|
|
@endphp
|
|
|
|
<div class="relative group">
|
|
@if($isImage)
|
|
<img src="{{ $attachment->temporaryUrl() }}" alt="Preview" class="w-16 h-16 object-cover rounded-lg border-2 border-gray-300 dark:border-gray-600">
|
|
@elseif($isVideo)
|
|
<div class="w-16 h-16 bg-gray-800 rounded-lg border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z"/>
|
|
</svg>
|
|
</div>
|
|
@else
|
|
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-lg border-2 border-blue-300 dark:border-blue-600 flex flex-col items-center justify-center p-1">
|
|
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
|
</svg>
|
|
<span class="text-[8px] font-medium text-blue-700 dark:text-blue-300 mt-0.5 uppercase">{{ $extension }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
<button
|
|
type="button"
|
|
wire:click="removeAttachment({{ $index }})"
|
|
class="absolute -top-1 -right-1 bg-red-500 hover:bg-red-600 text-white rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg"
|
|
>
|
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-[8px] px-1 py-0.5 rounded-b-lg truncate">
|
|
{{ Str::limit($attachment->getClientOriginalName(), 12) }}
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<form wire:submit.prevent="sendMessage" class="flex items-center gap-2">
|
|
<input
|
|
type="file"
|
|
wire:model="attachments"
|
|
id="bubble-attachment-input-{{ $selectedConversation }}"
|
|
multiple
|
|
accept="image/*,video/*,.txt,.php,.js,.css,.html,.json,.xml,.md,.zip"
|
|
class="hidden"
|
|
>
|
|
<button
|
|
type="button"
|
|
onclick="document.getElementById('bubble-attachment-input-{{ $selectedConversation }}').click()"
|
|
class="text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 transition-colors flex-shrink-0"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
|
</svg>
|
|
</button>
|
|
<input type="text"
|
|
wire:model="newMessage"
|
|
placeholder="Type a message..."
|
|
class="flex-1 px-3 py-1.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-full focus:ring-2 focus:ring-blue-500 focus:bg-white dark:focus:bg-gray-600 dark:text-white placeholder-gray-500 transition-all text-sm"
|
|
wire:keydown.enter.prevent="sendMessage">
|
|
<button type="submit"
|
|
class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full transition-colors shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
|
wire:loading.attr="disabled">
|
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" wire:loading.remove>
|
|
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
|
|
</svg>
|
|
<svg class="animate-spin w-4 h-4" wire:loading xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
@error('newMessage')
|
|
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
|
@enderror
|
|
@error('attachments.*')
|
|
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
|
@enderror
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Bubble Button -->
|
|
@if(!$isOpen)
|
|
<button wire:click="toggleBubble"
|
|
class="bg-gradient-to-br from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg transition-all hover:shadow-xl hover:scale-110 relative"
|
|
style="position: fixed; bottom: 16px; right: 16px; z-index: 9999;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-7 h-7">
|
|
<path d="M7.29117 20.8242L2 22L3.17581 16.7088C2.42544 15.3056 2 13.7025 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C10.2975 22 8.6944 21.5746 7.29117 20.8242ZM7.58075 18.711L8.23428 19.0605C9.38248 19.6745 10.6655 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 13.3345 4.32549 14.6175 4.93949 15.7657L5.28896 16.4192L4.63416 19.3658L7.58075 18.711Z"></path>
|
|
</svg>
|
|
@if($unreadCount > 0)
|
|
<span class="absolute -top-1 -right-1 bg-gradient-to-br from-red-500 to-red-600 text-white text-xs font-bold rounded-full min-w-[1.5rem] h-6 px-1.5 flex items-center justify-center shadow-lg animate-pulse border-2 border-white">
|
|
{{ $unreadCount > 99 ? '99+' : $unreadCount }}
|
|
</span>
|
|
@endif
|
|
</button>
|
|
@endif
|
|
|
|
<script>
|
|
function scrollBubbleToBottom(force = false) {
|
|
const messagesContainer = document.getElementById('bubble-messages');
|
|
if (!messagesContainer) return;
|
|
|
|
// Check if user is already at the bottom (within 50px threshold)
|
|
const isAtBottom = messagesContainer.scrollHeight - messagesContainer.clientHeight <= messagesContainer.scrollTop + 50;
|
|
|
|
// Scroll if forced or user is already at bottom
|
|
if (force || isAtBottom) {
|
|
messagesContainer.scrollTo({
|
|
top: messagesContainer.scrollHeight,
|
|
behavior: force ? 'instant' : 'smooth'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Auto-scroll to bottom when new messages arrive
|
|
document.addEventListener('livewire:initialized', () => {
|
|
Livewire.on('messageSent', () => {
|
|
setTimeout(() => scrollBubbleToBottom(true), 100);
|
|
});
|
|
|
|
// Listen for messages loaded
|
|
Livewire.on('messagesLoaded', () => {
|
|
setTimeout(() => scrollBubbleToBottom(true), 200);
|
|
});
|
|
|
|
// Scroll when conversation is loaded
|
|
Livewire.hook('morph.updated', ({ el, component }) => {
|
|
if (el && el.id === 'bubble-messages') {
|
|
setTimeout(() => scrollBubbleToBottom(true), 150);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Scroll to bottom on initial load and when bubble opens
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => scrollBubbleToBottom(true), 400);
|
|
});
|
|
|
|
// Scroll to bottom when Livewire finishes loading content
|
|
document.addEventListener('livewire:load', () => {
|
|
setTimeout(() => scrollBubbleToBottom(true), 300);
|
|
});
|
|
|
|
// Watch for bubble opening with more aggressive checking
|
|
setInterval(() => {
|
|
const container = document.getElementById('bubble-messages');
|
|
if (container && container.offsetParent !== null && container.children.length > 0) {
|
|
// Check if not already scrolled
|
|
const isScrolled = container.scrollTop > 0;
|
|
if (!isScrolled) {
|
|
scrollBubbleToBottom(true);
|
|
}
|
|
}
|
|
}, 500);
|
|
</script>
|
|
|
|
<!-- Report User Component -->
|
|
@if($selectedConversation && isset($recentConversations))
|
|
@php
|
|
$currentConversation = collect($recentConversations)->firstWhere('id', $selectedConversation);
|
|
$otherUser = $currentConversation['other_user'] ?? null;
|
|
@endphp
|
|
@if($otherUser)
|
|
<livewire:socialbase.report-user :userId="$otherUser['id']" :key="'report-user-bubble-'.$otherUser['id']" />
|
|
@endif
|
|
@endif
|
|
|
|
<!-- View Members Modal -->
|
|
@if($showMembersModal)
|
|
<div class="fixed inset-0 z-[10000] overflow-y-auto">
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" wire:click="closeViewMembersModal"></div>
|
|
|
|
<div class="relative inline-block w-full max-w-sm my-8 text-left align-middle transition-all transform bg-white dark:bg-gray-800 rounded-2xl shadow-2xl">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Group Members</h3>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Manage group members</p>
|
|
</div>
|
|
<button wire:click="closeViewMembersModal" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-4">
|
|
<div class="space-y-2 max-h-64 overflow-y-auto socialbase-scroll">
|
|
@foreach($groupMembers as $member)
|
|
<div class="flex items-center justify-between p-3 rounded-lg transition-colors {{ $member['is_creator'] ? 'bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800' : 'bg-gray-50 dark:bg-gray-700/50' }}">
|
|
<div class="flex items-center gap-2">
|
|
<img src="{{ $member['avatar'] }}" alt="{{ $member['name'] }}" class="w-8 h-8 rounded-full object-cover">
|
|
<div>
|
|
<p class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
{{ $member['name'] }}
|
|
@if($member['is_creator'])
|
|
<span class="text-[10px] bg-purple-500 text-white px-1.5 py-0.5 rounded-full">Creator</span>
|
|
@endif
|
|
</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $member['email'] }}</p>
|
|
</div>
|
|
</div>
|
|
@if(!$member['is_creator'] && isset($recentConversations))
|
|
@php
|
|
$conv = collect($recentConversations)->firstWhere('id', $selectedConversation);
|
|
@endphp
|
|
@if($conv && Auth::id() == ($conv['created_by'] ?? null))
|
|
<button
|
|
wire:click="removeMember({{ $member['id'] }})"
|
|
wire:confirm="Remove this member from the group?"
|
|
class="text-red-600 hover:text-red-700 dark:text-red-400 p-1.5 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
title="Remove member">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
|
|
<button wire:click="closeViewMembersModal" class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg text-sm font-medium transition-colors">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Add Members Modal -->
|
|
@if($showAddMembersModal)
|
|
<div class="fixed inset-0 z-[10000] overflow-y-auto" x-data="{ searchQuery: '' }">
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" wire:click="closeAddMembersModal"></div>
|
|
|
|
<div class="relative inline-block w-full max-w-sm my-8 text-left align-middle transition-all transform bg-white dark:bg-gray-800 rounded-2xl shadow-2xl">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Add Members</h3>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Invite people to the group</p>
|
|
</div>
|
|
<button wire:click="closeAddMembersModal" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-4 space-y-3">
|
|
<!-- Search Input -->
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">Search Users</label>
|
|
<input
|
|
type="text"
|
|
x-model="searchQuery"
|
|
placeholder="Search users by name..."
|
|
class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 dark:text-white placeholder-gray-400 transition-all">
|
|
</div>
|
|
|
|
<!-- Users List -->
|
|
<div class="space-y-1.5 max-h-48 overflow-y-auto socialbase-scroll">
|
|
@foreach($availableUsers as $user)
|
|
<div
|
|
x-show="searchQuery === '' || '{{ strtolower($user['name']) }}'.includes(searchQuery.toLowerCase())"
|
|
class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
|
wire:click="toggleUserToAdd({{ $user['id'] }})">
|
|
<div class="relative flex-shrink-0">
|
|
<div class="w-4 h-4 border-2 rounded {{ in_array($user['id'], $selectedUsersToAdd) ? 'bg-purple-600 border-purple-600' : 'border-gray-300 dark:border-gray-600' }} flex items-center justify-center transition-all">
|
|
@if(in_array($user['id'], $selectedUsersToAdd))
|
|
<svg class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<img src="{{ $user['avatar'] }}" alt="{{ $user['name'] }}" class="w-8 h-8 rounded-full object-cover flex-shrink-0">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{{ $user['name'] }}</p>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<!-- Selected Count -->
|
|
@if(count($selectedUsersToAdd) > 0)
|
|
<div class="p-2.5 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
|
<p class="text-xs font-medium text-purple-700 dark:text-purple-300">
|
|
{{ count($selectedUsersToAdd) }} {{ count($selectedUsersToAdd) === 1 ? 'user' : 'users' }} selected
|
|
</p>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-2 pt-3 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
wire:click="closeAddMembersModal"
|
|
class="flex-1 px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg text-sm font-medium transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button
|
|
wire:click="addMembersToGroup"
|
|
class="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors"
|
|
{{ count($selectedUsersToAdd) === 0 ? 'disabled' : '' }}>
|
|
Add Members
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Rename Group Modal -->
|
|
@if($showRenameModal)
|
|
<div class="fixed inset-0 z-[10000] overflow-y-auto">
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" wire:click="closeRenameModal"></div>
|
|
|
|
<div class="relative inline-block w-full max-w-sm my-8 text-left align-middle transition-all transform bg-white dark:bg-gray-800 rounded-2xl shadow-2xl">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Rename Group</h3>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Choose a new name</p>
|
|
</div>
|
|
<button wire:click="closeRenameModal" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form wire:submit.prevent="renameGroup" class="p-4 space-y-4">
|
|
<div>
|
|
<label for="newGroupName" class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
|
Group Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="newGroupName"
|
|
wire:model="newGroupName"
|
|
class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 dark:text-white placeholder-gray-400 transition-all"
|
|
placeholder="Enter new group name...">
|
|
@error('newGroupName')
|
|
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-3 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
type="button"
|
|
wire:click="closeRenameModal"
|
|
class="flex-1 px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg text-sm font-medium transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium transition-colors">
|
|
Rename
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|