typeName = $typeName; $this->count = $reactionCounter ?? 0; $this->showCounter = $showCounter; $this->inverseColors = $inverseColors; $this->redirectUrl = $redirectUrl; $this->activeProfile = getActiveProfile(); // Handle different ways of passing model data if ($modelInstance !== null) { // Traditional way: full model instance passed $this->targetModel = $modelInstance; // For reserve reactions on Posts, ensure meeting is loaded if ($typeName === 'reserve' && $modelInstance instanceof \App\Models\Post) { if (!$modelInstance->relationLoaded('meeting')) { $this->targetModel->load('meeting'); } } } elseif ($modelClass !== null && $modelId !== null) { // New way: class and ID passed separately $this->modelClass = $modelClass; $this->modelId = $modelId; try { if ($typeName === 'reserve' && $modelClass === 'App\Models\Post') { $this->targetModel = $modelClass::with('meeting')->find($modelId); } else { $this->targetModel = $modelClass::find($modelId); } } catch (\Exception $e) { \Log::error('ReactionButton: Failed to load model: ' . $e->getMessage()); $this->targetModel = null; } } else { $this->targetModel = null; } // If model not found or invalid, set component as disabled and return early if (!$this->targetModel || !is_object($this->targetModel)) { $this->isEnabled = false; $this->canReact = false; $this->disabledReason = 'model_not_found'; return; } $this->initializeReactionSettings(); $this->checkReactionPermissions(); $this->checkIfReacted(); } private function initializeReactionSettings() { // Check if this reaction type is enabled in config $this->isEnabled = timebank_config("reactions.{$this->typeName}.enabled", false); // Check if interaction is required for this reaction type $this->requiresInteraction = timebank_config("reactions.{$this->typeName}.only_with_interaction", false); // Get the interaction method to use for checking $this->interactionMethod = timebank_config("reactions.{$this->typeName}.interaction", 'hasTransactionsWith'); } private function checkReactionPermissions() { // If reaction type is not enabled, early return if (!$this->isEnabled) { $this->canReact = false; $this->disabledReason = 'reaction_disabled'; return; } // Guests cannot react if (!$this->activeProfile) { $this->canReact = false; $this->disabledReason = 'not_logged_in'; return; } // Admins cannot react if ($this->activeProfile instanceof \App\Models\Admin) { $this->canReact = false; $this->disabledReason = 'admin_cannot_react'; return; } // Profile must be registered as love reacter if ( !method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') || !$this->activeProfile->isRegisteredAsLoveReacter() ) { $this->canReact = false; $this->disabledReason = 'not_registered_reacter'; return; } // Cannot react to your own profile/model if ($this->isOwnProfile()) { $this->canReact = false; $this->disabledReason = 'cannot_react_own_profile'; return; } // Check interaction requirement if ($this->requiresInteraction) { // Use the configured interaction method if (method_exists($this->activeProfile, $this->interactionMethod)) { $this->hasInteraction = $this->activeProfile->{$this->interactionMethod}($this->targetModel); if (!$this->hasInteraction) { $this->canReact = false; $this->disabledReason = 'no_interaction'; return; } } else { // Fallback if method doesn't exist - treat as no interaction $this->canReact = false; $this->disabledReason = 'no_interaction'; return; } } $this->canReact = true; $this->disabledReason = null; } private function isOwnProfile() { // Safety check - ensure we have a valid model object if (!$this->targetModel || !is_object($this->targetModel) || !isset($this->targetModel->id)) { return false; } try { // Get the model class name safely $modelClass = get_class($this->targetModel); $modelId = $this->targetModel->id; return session('activeProfileType') === $modelClass && session('activeProfileId') === $modelId; } catch (\Exception $e) { \Log::error('ReactionButton isOwnProfile error: ' . $e->getMessage()); return false; } } private function checkIfReacted() { if (!$this->canReact || !$this->activeProfile->isRegisteredAsLoveReacter()) { $this->reactedByReactor = false; return; } $this->reactedByReactor = $this->activeProfile ->viaLoveReacter() ->hasReactedTo($this->targetModel, $this->typeName); } public function react() { // For reserve reaction type, show confirmation modal first if ($this->typeName === 'reserve') { $this->showConfirmModal = true; return; } $this->confirmReaction(); } public function confirmReaction() { // Close modal if open $this->showConfirmModal = false; // Security checks if (!$this->canReactSecurityCheck()) { return; } // Don't react if already reacted if ($this->reactedByReactor) { return; } try { $this->activeProfile->viaLoveReacter()->reactTo($this->targetModel, $this->typeName); // Update count and reacted state $this->count++; $this->reactedByReactor = true; // Optional: dispatch browser event for success feedback $this->dispatch('reaction-added', [ 'type' => $this->typeName, 'model' => get_class($this->targetModel), 'modelId' => $this->targetModel->id ]); } catch (\Exception $e) { // Log error and show user-friendly message \Log::error('Failed to add reaction: ' . $e->getMessage()); $this->dispatch('reaction-error', ['message' => 'Failed to add reaction. Please try again.']); } } public function unReact() { // Security checks if (!$this->canReactSecurityCheck()) { return; } // Don't unreact if not reacted if (!$this->reactedByReactor) { return; } try { $this->activeProfile->viaLoveReacter()->unReactTo($this->targetModel, $this->typeName); $this->count = max(0, $this->count - 1); // Prevent negative counts $this->reactedByReactor = false; // Optional: dispatch browser event for success feedback $this->dispatch('reaction-removed', [ 'type' => $this->typeName, 'model' => get_class($this->targetModel), 'modelId' => $this->targetModel->id ]); } catch (\Exception $e) { // Log error and show user-friendly message \Log::error('Failed to remove reaction: ' . $e->getMessage()); $this->dispatch('reaction-error', ['message' => 'Failed to remove reaction. Please try again.']); } } private function canReactSecurityCheck() { // Re-run all security checks on each action for security if (!$this->targetModel || (method_exists($this->targetModel, 'isRemoved') && $this->targetModel->isRemoved())) { return false; } if (!$this->isEnabled) { return false; } if ($this->activeProfile instanceof \App\Models\Admin) { return false; } if ( !method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') || !$this->activeProfile->isRegisteredAsLoveReacter() ) { return false; } if ($this->isOwnProfile()) { return false; } if ( $this->requiresInteraction && method_exists($this->activeProfile, $this->interactionMethod) && !$this->activeProfile->{$this->interactionMethod}($this->targetModel) ) { return false; } return true; } private function getDisabledReasonText() { // Only show text for specific disabled reasons that should be visible to users $visibleReasons = [ 'cannot_react_own_profile', 'no_interaction' ]; if (!in_array($this->disabledReason, $visibleReasons)) { return null; // Don't show text for other reasons } // Get reaction-specific disabled reason text $reason = timebank_config("reactions.{$this->typeName}.disabled_reasons.{$this->disabledReason}"); // Fallback to default messages if reaction-specific ones don't exist if (!$reason) { $fallbackReasons = [ 'cannot_react_own_profile' => __('You cannot react to your own content.'), 'no_interaction' => __('You need an interaction to react.'), ]; $reason = $fallbackReasons[$this->disabledReason] ?? ''; } return $reason; } private function shouldShowComponent() { // Hide component if model is not loaded if (!$this->targetModel) { return false; } // Hide component for these reasons instead of showing disabled state $hiddenReasons = [ 'reaction_disabled', 'admin_cannot_react', 'not_registered_reacter', 'model_not_found' ]; return !in_array($this->disabledReason, $hiddenReasons); } private function getIconSvg($typeName) { // Get icon file from config $iconFile = timebank_config("reactions.{$typeName}.icon_file"); if (!$iconFile) { return ''; } $svgPath = public_path('storage/app-images/' . $iconFile); if (!file_exists($svgPath)) { return ''; } $svgContent = file_get_contents($svgPath); // Extract only the path content from the SVG (remove outer svg tag) if (preg_match('/]*>.*?<\/path>|]*\/>/s', $svgContent, $matches)) { return $matches[0]; } // If no path found, try to extract circle or other shapes if (preg_match('/<(?:circle|rect|polygon|line)[^>]*>.*?<\/(?:circle|rect|polygon|line)>|<(?:circle|rect|polygon|line)[^>]*\/>/s', $svgContent, $matches)) { return $matches[0]; } return $svgContent; } public function render() { $iconSvg = $this->getIconSvg($this->typeName); $shouldShow = $this->shouldShowComponent(); $disabledReasonText = $this->getDisabledReasonText(); return view('livewire.reaction-button', [ 'iconSvg' => $iconSvg, 'disabledReasonText' => $disabledReasonText, 'shouldShow' => $shouldShow, 'inverseColors' => $this->inverseColors, 'redirectUrl' => $this->redirectUrl, ])->layout('layouts.app'); } }