Files
timebank-cc-public/app/Http/Livewire/ReactionButton.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

388 lines
13 KiB
PHP

<?php
namespace App\Http\Livewire;
use Livewire\Component;
class ReactionButton extends Component
{
public $targetModel = null;
public $modelClass = null;
public $modelId = null;
public $count = 0;
public $activeProfile;
public $reactedByReactor = false;
public $typeName;
public $showCounter = true;
public $isEnabled = false;
public $canReact = false;
public $requiresInteraction = false;
public $hasInteraction = false;
public $interactionMethod = null;
public $disabledReason = null;
public $size = 'w-10 h-10'; // Default size, can be overridden
public $showConfirmModal = false;
public $inverseColors = false; // Use light/background colors instead of reaction colors
public ?string $redirectUrl = null; // When set and user is not logged in, clicking redirects here
public function mount($typeName = 'like', $showCounter = true, $reactionCounter = null, $modelInstance = null, $modelClass = null, $modelId = null, $inverseColors = false, $redirectUrl = null)
{
$this->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[^>]*>.*?<\/path>|<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');
}
}