Initial commit
This commit is contained in:
364
app/Http/Livewire/WireChat/Chat/Chat.php
Normal file
364
app/Http/Livewire/WireChat/Chat/Chat.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\WireChat\Chat;
|
||||
|
||||
use App\Http\Livewire\WireChat\Chats\Chats;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Namu\WireChat\Livewire\Chat\Chat as BaseChat;
|
||||
|
||||
class Chat extends BaseChat
|
||||
{
|
||||
|
||||
// This magic accessor will be called for $this->auth
|
||||
public function getAuthProperty()
|
||||
{
|
||||
// Use the active guard from session first, then fallback to checking all guards
|
||||
$activeGuard = session('active_guard', 'web');
|
||||
$user = Auth::guard($activeGuard)->user();
|
||||
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Fallback to checking all guards if active guard doesn't have a user
|
||||
return Auth::guard('admin')->user()
|
||||
?: Auth::guard('bank')->user()
|
||||
?: Auth::guard('organization')->user()
|
||||
?: Auth::guard('web')->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check authorization and show error toast if unauthorized
|
||||
*/
|
||||
private function checkAuthorization(): bool
|
||||
{
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
$this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
return true;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
$this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Override sendMessage to use the correct sendable_id.
|
||||
*/
|
||||
public function sendMessage()
|
||||
{
|
||||
abort_unless($this->auth(), 401);
|
||||
|
||||
// CRITICAL SECURITY: Validate authorization before sending message
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
// rate limit
|
||||
$this->rateLimit();
|
||||
|
||||
$attachments = array_merge($this->media, $this->files);
|
||||
|
||||
if (empty($attachments)) {
|
||||
$this->validate(['body' => 'required|string']);
|
||||
}
|
||||
|
||||
if (count($attachments) != 0) {
|
||||
$maxUploads = config('wirechat.attachments.max_uploads');
|
||||
$fileMimes = implode(',', config('wirechat.attachments.file_mimes'));
|
||||
$fileMaxUploadSize = (int) config('wirechat.attachments.file_max_upload_size');
|
||||
$mediaMimes = implode(',', config('wirechat.attachments.media_mimes'));
|
||||
$mediaMaxUploadSize = (int) config('wirechat.attachments.media_max_upload_size');
|
||||
|
||||
try {
|
||||
$this->validate([
|
||||
'files' => "array|max:$maxUploads|nullable",
|
||||
'files.*' => "max:$fileMaxUploadSize|mimes:$fileMimes",
|
||||
'media' => "array|max:$maxUploads|nullable",
|
||||
'media.*' => "max:$mediaMaxUploadSize|mimes:$mediaMimes",
|
||||
]);
|
||||
} catch (\Illuminate\Validation\ValidationException $th) {
|
||||
$errors = $th->errors();
|
||||
foreach ($errors as $field => $messages) {
|
||||
$this->addError($field, $messages[0]);
|
||||
}
|
||||
return $this->dispatch('wirechat-toast', type: 'warning', message: $th->getMessage());
|
||||
}
|
||||
|
||||
$createdMessages = [];
|
||||
foreach ($attachments as $key => $attachment) {
|
||||
$path = $attachment->store(
|
||||
\Namu\WireChat\Facades\WireChat::storageFolder(),
|
||||
\Namu\WireChat\Facades\WireChat::storageDisk()
|
||||
);
|
||||
|
||||
$replyId = ($key === 0 && $this->replyMessage) ? $this->replyMessage->id : null;
|
||||
|
||||
$message = \Namu\WireChat\Models\Message::create([
|
||||
'reply_id' => $replyId,
|
||||
'conversation_id' => $this->conversation->id,
|
||||
'sendable_type' => $this->auth->getMorphClass(), // Polymorphic sender type
|
||||
'sendable_id' => $this->auth->id, // Polymorphic sender ID
|
||||
'type' => \Namu\WireChat\Enums\MessageType::ATTACHMENT,
|
||||
]);
|
||||
|
||||
$attachment = $message->attachment()->create([
|
||||
'file_path' => $path,
|
||||
'file_name' => basename($path),
|
||||
'original_name' => $attachment->getClientOriginalName(),
|
||||
'mime_type' => $attachment->getMimeType(),
|
||||
'url' => \Storage::disk(\Namu\WireChat\Facades\WireChat::storageDisk())->url($path),
|
||||
]);
|
||||
|
||||
$createdMessages[] = $message;
|
||||
$this->conversation->updated_at = now();
|
||||
$this->conversation->save();
|
||||
$this->dispatch('refresh')->to(Chats::class);
|
||||
$this->dispatch('refresh')->to(\App\Http\Livewire\UnreadIndicator::class);
|
||||
$this->dispatchMessageCreatedEvent($message);
|
||||
}
|
||||
|
||||
foreach ($createdMessages as $key => $message) {
|
||||
|
||||
$this->pushMessage($message);
|
||||
}
|
||||
$this->dispatch('scroll-bottom');
|
||||
}
|
||||
|
||||
if ($this->body != null) {
|
||||
$createdMessage = \Namu\WireChat\Models\Message::create([
|
||||
'reply_id' => $this->replyMessage?->id,
|
||||
'conversation_id' => $this->conversation->id,
|
||||
'sendable_type' => $this->auth->getMorphClass(), // Polymorphic sender type
|
||||
'sendable_id' => $this->auth->id, // Polymorphic sender ID
|
||||
'body' => $this->body,
|
||||
'type' => \Namu\WireChat\Enums\MessageType::TEXT,
|
||||
]);
|
||||
|
||||
$this->pushMessage($createdMessage);
|
||||
$this->conversation->touch();
|
||||
$this->dispatchMessageCreatedEvent($createdMessage);
|
||||
$this->dispatch('refresh')->to(Chats::class);
|
||||
$this->dispatch('refreshList')->to(Chats::class);
|
||||
|
||||
// Also dispatch to UnreadIndicator to update the red dot
|
||||
$this->dispatch('refresh')->to(\App\Http\Livewire\UnreadIndicator::class);
|
||||
|
||||
}
|
||||
|
||||
$this->reset('media', 'files', 'body');
|
||||
$this->dispatch('scroll-bottom');
|
||||
|
||||
$this->removeReply();
|
||||
}
|
||||
|
||||
|
||||
private function pushMessage(\Namu\WireChat\Models\Message $message)
|
||||
{
|
||||
$groupKey = $this->messageGroupKey($message);
|
||||
|
||||
// Ensure loadedMessages is a Collection
|
||||
$this->loadedMessages = collect($this->loadedMessages);
|
||||
|
||||
// Use tap to create a new group if it doesn’t exist, then push the message
|
||||
$this->loadedMessages->put($groupKey, $this->loadedMessages->get($groupKey, collect())->push($message));
|
||||
}
|
||||
|
||||
|
||||
private function messageGroupKey(\Namu\WireChat\Models\Message $message): string
|
||||
{
|
||||
$messageDate = $message->created_at;
|
||||
$groupKey = '';
|
||||
if ($messageDate->isToday()) {
|
||||
$groupKey = __('wirechat::chat.message_groups.today');
|
||||
} elseif ($messageDate->isYesterday()) {
|
||||
$groupKey = __('wirechat::chat.message_groups.yesterday');
|
||||
} elseif ($messageDate->greaterThanOrEqualTo(now()->subDays(7))) {
|
||||
$groupKey = $messageDate->format('l'); // Day name
|
||||
} else {
|
||||
$groupKey = $messageDate->format('d/m/Y'); // Older than 7 days, dd/mm/yyyy
|
||||
}
|
||||
|
||||
return $groupKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Keep a message from auto-deletion (mark as kept)
|
||||
*/
|
||||
public function keepMessage(string $id): void
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->auth()) {
|
||||
$this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access'));
|
||||
return;
|
||||
}
|
||||
|
||||
$id = decrypt($id);
|
||||
$message = \Namu\WireChat\Models\Message::findOrFail($id);
|
||||
|
||||
// Verify user belongs to the conversation
|
||||
if (!$this->auth->belongsToConversation($message->conversation)) {
|
||||
$this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if disappearing messages is enabled and allow_users_to_keep is true
|
||||
if (!timebank_config('wirechat.disappearing_messages.enabled', true) ||
|
||||
!timebank_config('wirechat.disappearing_messages.allow_users_to_keep', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle kept status
|
||||
if ($message->kept_at) {
|
||||
// Unkeep the message
|
||||
$message->kept_at = null;
|
||||
} else {
|
||||
// Keep the message
|
||||
$message->kept_at = now();
|
||||
}
|
||||
|
||||
$message->save();
|
||||
|
||||
// Update the message in loadedMessages collection to trigger Livewire re-render
|
||||
if ($this->loadedMessages) {
|
||||
$this->loadedMessages = collect($this->loadedMessages)->map(function ($messageGroup) use ($message) {
|
||||
return $messageGroup->map(function ($loadedMessage) use ($message) {
|
||||
if ($loadedMessage->id === $message->id) {
|
||||
$loadedMessage->kept_at = $message->kept_at;
|
||||
}
|
||||
return $loadedMessage;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete conversation (override with authorization)
|
||||
*/
|
||||
public function deleteConversation()
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::deleteConversation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversation history (override with authorization)
|
||||
*/
|
||||
public function clearConversation()
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::clearConversation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit conversation (override with authorization)
|
||||
*/
|
||||
public function exitConversation()
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::exitConversation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete message for me (override with authorization)
|
||||
*/
|
||||
public function deleteForMe(string $id): void
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::deleteForMe($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete message for everyone (override with authorization)
|
||||
*/
|
||||
public function deleteForEveryone(string $id): void
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::deleteForEveryone($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send like (override with authorization)
|
||||
*/
|
||||
public function sendLike()
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::sendLike();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set reply (override with authorization)
|
||||
*/
|
||||
public function setReply(string $id): void
|
||||
{
|
||||
if (!$this->checkAuthorization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::setReply($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount component (override with authorization)
|
||||
*/
|
||||
public function mount($conversation = null)
|
||||
{
|
||||
// CRITICAL SECURITY: Validate authorization on mount
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
return parent::mount($conversation);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
// CRITICAL SECURITY: Re-validate authorization on every render
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
return view('errors.unauthorized-component');
|
||||
}
|
||||
|
||||
try {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
return view('errors.unauthorized-component');
|
||||
}
|
||||
|
||||
return parent::render();
|
||||
}
|
||||
|
||||
}
|
||||
115
app/Http/Livewire/WireChat/Chats/Chats.php
Normal file
115
app/Http/Livewire/WireChat/Chats/Chats.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\WireChat\Chats;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Namu\WireChat\Livewire\Chats\Chats as BaseChats;
|
||||
|
||||
class Chats extends BaseChats
|
||||
{
|
||||
/**
|
||||
* Get all listeners including parent's Echo listeners
|
||||
*/
|
||||
public function getListeners()
|
||||
{
|
||||
// Get parent's listeners (includes Echo channels)
|
||||
$parentListeners = parent::getListeners();
|
||||
|
||||
// Debug: Log what listeners are being registered
|
||||
\Log::info('WireChat Chats Listeners', [
|
||||
'parent_listeners' => $parentListeners,
|
||||
'active_guard' => session('active_guard', 'web'),
|
||||
'auth_user_id' => $this->auth?->id,
|
||||
'auth_user_class' => $this->auth ? get_class($this->auth) : null,
|
||||
]);
|
||||
|
||||
// Add our custom listener
|
||||
return array_merge($parentListeners, [
|
||||
'refreshList' => 'handleRefreshList',
|
||||
]);
|
||||
}
|
||||
|
||||
// This magic accessor will be called for $this->auth
|
||||
public function getAuthProperty()
|
||||
{
|
||||
// Use the active guard from session first, then fallback to checking all guards
|
||||
$activeGuard = session('active_guard', 'web');
|
||||
$user = Auth::guard($activeGuard)->user();
|
||||
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Fallback to checking all guards if active guard doesn't have a user
|
||||
return Auth::guard('admin')->user()
|
||||
?: Auth::guard('bank')->user()
|
||||
?: Auth::guard('organization')->user()
|
||||
?: Auth::guard('web')->user();
|
||||
}
|
||||
|
||||
|
||||
public function handleRefreshList()
|
||||
{
|
||||
// Simply dispatch a refresh event to update the component
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override loadConversations to fix lastMessage loading for non-web guards
|
||||
* The parent uses auth()->user() which defaults to web guard, causing lastMessage to be null for admin/bank/org guards
|
||||
*/
|
||||
protected function loadConversations()
|
||||
{
|
||||
// Call parent to load conversations
|
||||
parent::loadConversations();
|
||||
|
||||
// Manually load lastMessage for each conversation without global scopes
|
||||
// This fixes the issue where WithoutRemovedMessages scope uses auth()->user() (web guard)
|
||||
$this->conversations->each(function ($conversation) {
|
||||
$lastMessage = \Namu\WireChat\Models\Message::withoutGlobalScopes()
|
||||
->where('conversation_id', $conversation->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($lastMessage) {
|
||||
$conversation->setRelation('lastMessage', $lastMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount component (override with authorization)
|
||||
*/
|
||||
public function mount(
|
||||
$showNewChatModalButton = null,
|
||||
$allowChatsSearch = null,
|
||||
$showHomeRouteButton = null,
|
||||
?string $title = null
|
||||
) {
|
||||
// CRITICAL SECURITY: Validate authorization on mount
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
return parent::mount($showNewChatModalButton, $allowChatsSearch, $showHomeRouteButton, $title);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
// CRITICAL SECURITY: Re-validate authorization on every render
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
return view('errors.unauthorized-component');
|
||||
}
|
||||
|
||||
try {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
return view('errors.unauthorized-component');
|
||||
}
|
||||
|
||||
return parent::render();
|
||||
}
|
||||
}
|
||||
143
app/Http/Livewire/WireChat/DisappearingMessagesSettings.php
Normal file
143
app/Http/Livewire/WireChat/DisappearingMessagesSettings.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\WireChat;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
use Namu\WireChat\Models\Conversation;
|
||||
|
||||
class DisappearingMessagesSettings extends Component
|
||||
{
|
||||
public $conversationId;
|
||||
public $conversation;
|
||||
public $isEnabled = false;
|
||||
public $selectedDuration;
|
||||
public $allowUsersToKeep = true;
|
||||
public $userControl = false;
|
||||
public $platformDuration;
|
||||
public $platformEnabled = true;
|
||||
|
||||
protected $listeners = ['refreshDisappearing' => '$refresh'];
|
||||
|
||||
// Multi-guard authentication support
|
||||
public function getAuthProperty()
|
||||
{
|
||||
return Auth::guard('admin')->user()
|
||||
?: Auth::guard('bank')->user()
|
||||
?: Auth::guard('organization')->user()
|
||||
?: Auth::guard('web')->user();
|
||||
}
|
||||
|
||||
public function mount($conversationId)
|
||||
{
|
||||
$this->conversationId = $conversationId;
|
||||
$this->conversation = Conversation::findOrFail($conversationId);
|
||||
|
||||
// Check if user belongs to conversation
|
||||
$user = $this->auth;
|
||||
if (!$user || !$user->belongsToConversation($this->conversation)) {
|
||||
abort(403, 'You do not belong to this conversation');
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
$this->allowUsersToKeep = timebank_config('wirechat.disappearing_messages.allow_users_to_keep', true);
|
||||
$this->userControl = false; // Always false (hardcoded)
|
||||
$this->platformEnabled = true; // Always enabled (hardcoded)
|
||||
|
||||
// Get duration in days and convert to seconds
|
||||
$durationInDays = timebank_config('wirechat.disappearing_messages.duration', 30);
|
||||
$this->platformDuration = $durationInDays * 86400; // Convert days to seconds
|
||||
|
||||
// Load current settings
|
||||
$this->isEnabled = $this->conversation->hasDisappearingTurnedOn();
|
||||
$this->selectedDuration = $this->conversation->disappearing_duration ?? $this->platformDuration;
|
||||
|
||||
// Auto-enable if not yet enabled for this conversation (always enabled)
|
||||
if (!$this->isEnabled) {
|
||||
$this->enablePlatformDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable disappearing messages with platform default settings
|
||||
*/
|
||||
private function enablePlatformDefault()
|
||||
{
|
||||
try {
|
||||
$this->conversation->turnOnDisappearing($this->platformDuration);
|
||||
$this->isEnabled = true;
|
||||
$this->selectedDuration = $this->platformDuration;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Silently fail - this will be shown in the UI
|
||||
\Log::warning("Failed to enable disappearing messages for conversation {$this->conversationId}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable duration
|
||||
*/
|
||||
public function getFormattedDurationProperty()
|
||||
{
|
||||
$seconds = $this->selectedDuration ?? $this->platformDuration;
|
||||
|
||||
$days = floor($seconds / 86400);
|
||||
if ($days > 0) {
|
||||
return trans_choice('messages.wirechat.duration.day', $days, ['count' => $days]);
|
||||
}
|
||||
|
||||
$hours = floor($seconds / 3600);
|
||||
if ($hours > 0) {
|
||||
return trans_choice('messages.wirechat.duration.hour', $hours, ['count' => $hours]);
|
||||
}
|
||||
|
||||
$minutes = floor($seconds / 60);
|
||||
if ($minutes > 0) {
|
||||
return trans_choice('messages.wirechat.duration.minute', $minutes, ['count' => $minutes]);
|
||||
}
|
||||
|
||||
if ($seconds > 0) {
|
||||
return trans_choice('messages.wirechat.duration.second', $seconds, ['count' => $seconds]);
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable kept messages duration
|
||||
*/
|
||||
public function getFormattedKeptDurationProperty()
|
||||
{
|
||||
$daysFromConfig = timebank_config('wirechat.disappearing_messages.kept_messages_duration');
|
||||
|
||||
if ($daysFromConfig === null) {
|
||||
return null; // Never delete
|
||||
}
|
||||
|
||||
// Config is now in days, so use directly
|
||||
$days = $daysFromConfig;
|
||||
|
||||
// Convert to months/years if applicable
|
||||
// Check for years only if it's at least 2 full years (730 days)
|
||||
if ($days >= 730) {
|
||||
$years = floor($days / 365);
|
||||
return trans_choice('messages.wirechat.duration.year', $years, ['count' => $years]);
|
||||
}
|
||||
|
||||
// For 30+ days but less than 2 years, show in months
|
||||
if ($days >= 30) {
|
||||
$months = floor($days / 30);
|
||||
return trans_choice('messages.wirechat.duration.month', $months, ['count' => $months]);
|
||||
}
|
||||
|
||||
if ($days > 0) {
|
||||
return trans_choice('messages.wirechat.duration.day', $days, ['count' => $days]);
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.wire-chat.disappearing-messages-settings');
|
||||
}
|
||||
}
|
||||
70
app/Http/Livewire/WireChat/New/Chat.php
Normal file
70
app/Http/Livewire/WireChat/New/Chat.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\WireChat\New;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Namu\WireChat\Livewire\New\Chat as NewChat;
|
||||
|
||||
class Chat extends NewChat
|
||||
{
|
||||
// This magic accessor will be called for $this->auth
|
||||
public function getAuthProperty()
|
||||
{
|
||||
// Use the active guard from session first, then fallback to checking all guards
|
||||
$activeGuard = session('active_guard', 'web');
|
||||
$user = Auth::guard($activeGuard)->user();
|
||||
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Fallback to checking all guards if active guard doesn't have a user
|
||||
return Auth::guard('admin')->user()
|
||||
?: Auth::guard('bank')->user()
|
||||
?: Auth::guard('organization')->user()
|
||||
?: Auth::guard('web')->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount component (override with authorization)
|
||||
*/
|
||||
public function mount()
|
||||
{
|
||||
// CRITICAL SECURITY: Validate authorization on mount
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
return parent::mount();
|
||||
}
|
||||
|
||||
public function createConversation($id, string $class)
|
||||
{
|
||||
// CRITICAL SECURITY: Validate authorization before creating conversation
|
||||
$profile = getActiveProfile();
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
$model = app($class);
|
||||
$model = $model::find($id);
|
||||
|
||||
if ($model) {
|
||||
|
||||
$createdConversation = $this->auth->createConversationWith($model);
|
||||
|
||||
if ($createdConversation) {
|
||||
$this->closeWireChatModal();
|
||||
$this->handleComponentTermination(
|
||||
redirectRoute: route(\Namu\WireChat\Facades\WireChat::viewRouteName(), [$createdConversation->id]),
|
||||
events: [
|
||||
\Namu\WireChat\Livewire\Widgets\WireChat::class => ['open-chat', ['conversation' => $createdConversation->id]],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
app/Http/Livewire/WireChat/TypingIndicator.php
Normal file
331
app/Http/Livewire/WireChat/TypingIndicator.php
Normal file
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
// app/Http/Livewire/WireChat/TypingIndicator.php
|
||||
namespace App\Http\Livewire\WireChat;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Livewire\Component;
|
||||
|
||||
class TypingIndicator extends Component
|
||||
{
|
||||
// Public properties
|
||||
public $conversationId;
|
||||
public $typingUsers = [];
|
||||
public $currentUserId;
|
||||
public $currentUserName;
|
||||
public $currentUserType;
|
||||
public $currentUserAvatar;
|
||||
public $showAvatars = true;
|
||||
public $maxDisplay = 3;
|
||||
|
||||
protected $typingTimeout = 4;
|
||||
|
||||
public function mount($conversationId, $showAvatars = true, $maxDisplay = 3)
|
||||
{
|
||||
$this->conversationId = $conversationId;
|
||||
$this->showAvatars = $showAvatars;
|
||||
$this->maxDisplay = $maxDisplay;
|
||||
$this->typingUsers = [];
|
||||
|
||||
$user = auth()->user();
|
||||
if ($user) {
|
||||
$this->currentUserId = $user->id;
|
||||
$this->currentUserName = $user->name;
|
||||
$this->currentUserType = class_basename($user->getMorphClass());
|
||||
$this->currentUserAvatar = ($user->getMorphClass())::find($this->currentUserId)->profile_photo_path ?? null;
|
||||
}
|
||||
|
||||
$this->loadTypingUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache key for this user's typing status
|
||||
*/
|
||||
private function getCacheKey()
|
||||
{
|
||||
// Simplified cache key format without special characters
|
||||
return "wirechat_typing_{$this->conversationId}_{$this->currentUserType}_{$this->currentUserId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pattern for finding all typing users in this conversation
|
||||
*/
|
||||
private function getCachePattern()
|
||||
{
|
||||
return "wirechat_typing_{$this->conversationId}_*";
|
||||
}
|
||||
|
||||
/**
|
||||
* Start typing - called from JavaScript
|
||||
*/
|
||||
public function startTyping()
|
||||
{
|
||||
if (!$this->currentUserId) {
|
||||
\Log::warning('TypingIndicator: No current user ID');
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey();
|
||||
|
||||
$userData = [
|
||||
'user_id' => $this->currentUserId,
|
||||
'user_name' => $this->currentUserName,
|
||||
'user_type' => $this->currentUserType,
|
||||
'avatar' => $this->currentUserAvatar,
|
||||
'started_at' => now(),
|
||||
'conversation_id' => $this->conversationId,
|
||||
];
|
||||
|
||||
try {
|
||||
// Store with expiration
|
||||
Cache::put($cacheKey, $userData, now()->addSeconds($this->typingTimeout + 2));
|
||||
|
||||
// Immediately load typing users to update the display
|
||||
$this->loadTypingUsers();
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('TypingIndicator: Error starting typing', [
|
||||
'error' => $e->getMessage(),
|
||||
'cache_key' => $cacheKey
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop typing - called from JavaScript
|
||||
*/
|
||||
public function stopTyping()
|
||||
{
|
||||
if (!$this->currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey();
|
||||
|
||||
try {
|
||||
Cache::forget($cacheKey);
|
||||
|
||||
$this->loadTypingUsers();
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('TypingIndicator: Error stopping typing', [
|
||||
'error' => $e->getMessage(),
|
||||
'cache_key' => $cacheKey
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load typing users from cache
|
||||
*/
|
||||
public function loadTypingUsers()
|
||||
{
|
||||
try {
|
||||
$typingUsers = [];
|
||||
|
||||
// Get the correct Redis connection for cache
|
||||
$cacheRedisConnection = config('cache.stores.redis.connection', 'default');
|
||||
|
||||
// From the exact debugging, the actual format is:
|
||||
// Redis key: timebankcc_dev_on_thinkpad_p53_database_timebankcc_dev_on_thinkpad_p53_cache:wirechat_typing_1_User_161
|
||||
// But we search with: timebankcc_dev_on_thinkpad_p53_cache:wirechat_typing_1_*
|
||||
// And need to extract: wirechat_typing_1_User_161
|
||||
|
||||
$cachePrefix = config('cache.prefix', '');
|
||||
$databasePrefix = config('database.redis.options.prefix', '');
|
||||
$pattern = $this->getCachePattern();
|
||||
|
||||
// Use the working search pattern (cache prefix only)
|
||||
$searchPattern = $cachePrefix . ':' . $pattern;
|
||||
|
||||
// But the actual key prefix includes database prefix
|
||||
$actualKeyPrefix = $databasePrefix . $cachePrefix . ':';
|
||||
|
||||
if (config('cache.default') === 'redis') {
|
||||
// Use the CACHE Redis connection (database 1)
|
||||
$connection = Redis::connection($cacheRedisConnection);
|
||||
$redisKeys = $connection->keys($searchPattern);
|
||||
|
||||
foreach ($redisKeys as $redisKey) {
|
||||
// Extract the Laravel cache key by removing the ACTUAL prefix (database + cache)
|
||||
if (strpos($redisKey, $actualKeyPrefix) === 0) {
|
||||
$laravelCacheKey = substr($redisKey, strlen($actualKeyPrefix));
|
||||
} else {
|
||||
// Fallback - the key format might be different
|
||||
\Log::warning('TypingIndicator: Unexpected key format', [
|
||||
'redis_key' => $redisKey,
|
||||
'expected_prefix' => $actualKeyPrefix
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use Laravel Cache to get the data (it handles serialization properly)
|
||||
$userData = Cache::get($laravelCacheKey);
|
||||
|
||||
if ($userData && isset($userData['user_id']) && $userData['user_id'] != $this->currentUserId) {
|
||||
if (isset($userData['started_at']) && $userData['started_at']->diffInSeconds(now()) <= $this->typingTimeout) {
|
||||
$typingUsers[] = $userData;
|
||||
\Log::info('TypingIndicator: Added typing user', [
|
||||
'user_id' => $userData['user_id'],
|
||||
'user_name' => $userData['user_name']
|
||||
]);
|
||||
} else {
|
||||
// Clean up expired entries
|
||||
Cache::forget($laravelCacheKey);
|
||||
}
|
||||
} elseif ($userData && $userData['user_id'] == $this->currentUserId) {
|
||||
\Log::info('TypingIndicator: Ignoring current user\'s typing', [
|
||||
'current_user_id' => $this->currentUserId
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback for non-Redis cache drivers
|
||||
$registryKey = "typing_registry_{$this->conversationId}";
|
||||
$registry = Cache::get($registryKey, []);
|
||||
|
||||
foreach ($registry as $key) {
|
||||
$userData = Cache::get($key);
|
||||
if ($userData && isset($userData['user_id']) && $userData['user_id'] != $this->currentUserId) {
|
||||
if (isset($userData['started_at']) && $userData['started_at']->diffInSeconds(now()) <= $this->typingTimeout) {
|
||||
$typingUsers[] = $userData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->typingUsers = collect($typingUsers)->take($this->maxDisplay)->toArray();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->typingUsers = [];
|
||||
\Log::error('TypingIndicator: Error loading typing users', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get typing text for display
|
||||
*/
|
||||
public function getTypingText()
|
||||
{
|
||||
$count = is_array($this->typingUsers) ? count($this->typingUsers) : 0;
|
||||
|
||||
if ($count === 0) {
|
||||
return '';
|
||||
} elseif ($count === 1) {
|
||||
return ($this->typingUsers[0]['user_name'] ?? 'Someone') . ' is typing...';
|
||||
} elseif ($count === 2) {
|
||||
$name1 = $this->typingUsers[0]['user_name'] ?? 'Someone';
|
||||
$name2 = $this->typingUsers[1]['user_name'] ?? 'Someone';
|
||||
return $name1 . ' and ' . $name2 . ' are typing...';
|
||||
} else {
|
||||
$name1 = $this->typingUsers[0]['user_name'] ?? 'Someone';
|
||||
return $name1 . ' and ' . ($count - 1) . ' others are typing...';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual refresh method
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
$this->loadTypingUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced debug method
|
||||
*/
|
||||
public function debug()
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
$pattern = $this->getCachePattern();
|
||||
|
||||
// Get Redis connection info
|
||||
$cacheRedisConnection = config('cache.stores.redis.connection', 'default');
|
||||
$cachePrefix = config('cache.prefix', '');
|
||||
$databasePrefix = config('database.redis.options.prefix', '');
|
||||
|
||||
// Build the full pattern accounting for both prefixes
|
||||
$fullPattern = '';
|
||||
if ($databasePrefix) {
|
||||
$fullPattern .= $databasePrefix;
|
||||
}
|
||||
if ($cachePrefix) {
|
||||
$fullPattern .= $cachePrefix . ':';
|
||||
}
|
||||
$fullPattern .= $pattern;
|
||||
|
||||
$redisKeys = [];
|
||||
$redisData = [];
|
||||
|
||||
if (config('cache.default') === 'redis') {
|
||||
try {
|
||||
$connection = Redis::connection($cacheRedisConnection);
|
||||
|
||||
// Check for typing keys with the full pattern
|
||||
$allTypingKeys = $connection->keys($databasePrefix ? $databasePrefix . $cachePrefix . ':*typing*' : $cachePrefix . ':*typing*');
|
||||
$patternKeys = $connection->keys($fullPattern);
|
||||
|
||||
$redisKeys = [
|
||||
'all_typing_keys' => $allTypingKeys,
|
||||
'pattern_keys' => $patternKeys,
|
||||
'current_user_key' => $cacheKey,
|
||||
'full_pattern_searched' => $fullPattern,
|
||||
'cache_redis_connection' => $cacheRedisConnection
|
||||
];
|
||||
|
||||
foreach ($patternKeys as $key) {
|
||||
// Extract the Laravel cache key by removing both prefixes
|
||||
$cleanKey = $key;
|
||||
if ($databasePrefix && strpos($cleanKey, $databasePrefix) === 0) {
|
||||
$cleanKey = substr($cleanKey, strlen($databasePrefix));
|
||||
}
|
||||
if ($cachePrefix && strpos($cleanKey, $cachePrefix . ':') === 0) {
|
||||
$cleanKey = substr($cleanKey, strlen($cachePrefix . ':'));
|
||||
}
|
||||
|
||||
$data = Cache::get($cleanKey);
|
||||
if ($data) {
|
||||
$redisData[] = [
|
||||
'full_redis_key' => $key,
|
||||
'clean_cache_key' => $cleanKey,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('TypingIndicator: Redis debug error', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$debugInfo = [
|
||||
'conversationId' => $this->conversationId,
|
||||
'typingUsers' => $this->typingUsers,
|
||||
'typingUsersCount' => is_array($this->typingUsers) ? count($this->typingUsers) : 'not-array',
|
||||
'currentUserId' => $this->currentUserId,
|
||||
'currentUserName' => $this->currentUserName,
|
||||
'currentUserType' => $this->currentUserType,
|
||||
'showAvatars' => $this->showAvatars,
|
||||
'cacheDriver' => config('cache.default'),
|
||||
'cacheKey' => $cacheKey,
|
||||
'cachePattern' => $pattern,
|
||||
'cachePrefix' => $cachePrefix,
|
||||
'databasePrefix' => $databasePrefix,
|
||||
'fullPattern' => $fullPattern,
|
||||
'cacheRedisConnection' => $cacheRedisConnection,
|
||||
'redisKeys' => $redisKeys,
|
||||
'redisData' => $redisData,
|
||||
'typingTimeout' => $this->typingTimeout,
|
||||
];
|
||||
|
||||
\Log::info('TypingIndicator: Debug info', $debugInfo);
|
||||
|
||||
return $debugInfo;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.wire-chat.typing-indicator');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user