middleware('auth')->except(['channel']); } // Profile page - personal overview for the authenticated user public function profile() { $user = Auth::user(); return view('user.profile', compact('user')); } // Update profile public function updateProfile(Request $request) { $authUser = Auth::user(); // Super admins may edit any user's profile by passing _edit_user_id if ($authUser->isSuperAdmin() && $request->filled('_edit_user_id')) { $user = User::findOrFail($request->input('_edit_user_id')); } else { $user = $authUser; } $request->validate([ 'name' => 'required|string|max:255', 'avatar' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120', 'bio' => 'nullable|string|max:500', 'birthday' => 'nullable|date', 'location' => 'nullable|string|max:100', 'gender' => 'nullable|in:male,female,prefer_not_to_say', 'nationality' => 'nullable|string|size:2', 'phone_code' => 'nullable|string|max:20', 'phone_number' => 'nullable|string|max:30', 'timezone' => 'nullable|timezone:all', 'slink' => 'nullable|array', 'slink.*.platform' => 'required_with:slink|string|max:30', 'slink.*.value' => 'required_with:slink|string|max:500', 'slink.*.visibility' => 'nullable|in:public,registered,subscribers,only_me', ]); $data = [ 'name' => $request->name, 'bio' => $request->bio, 'birthday' => $request->birthday, 'location' => $request->location, 'gender' => $request->gender ?: null, 'nationality' => $request->nationality ?: null, 'phone_code' => $request->phone_code ?: null, 'phone_number' => $request->phone_number ?: null, 'timezone' => $request->timezone ?: null, ]; if ($request->hasFile('avatar')) { if ($user->avatar) { Storage::delete('public/avatars/'.$user->avatar); } $filename = self::generateFilename($request->file('avatar')->getClientOriginalExtension()); $request->file('avatar')->storeAs('public/avatars', $filename); $data['avatar'] = $filename; } $user->update($data); // Sync social links $user->socialLinks()->delete(); $order = 0; foreach ($request->input('slink', []) as $entry) { $platform = trim($entry['platform'] ?? ''); $value = trim($entry['value'] ?? ''); if ($platform && $value) { $user->socialLinks()->create([ 'platform' => $platform, 'value' => $value, 'visibility' => $entry['visibility'] ?? 'public', 'sort_order' => $order++, ]); } } // Redirect back to profile page, or channel settings if admin edited another user if ($authUser->isSuperAdmin() && $authUser->id !== $user->id) { return redirect()->route('channel', $user->channel)->with('toast_success', 'Profile updated!')->with('_open_tab', 'settings'); } return redirect()->route('profile')->with('toast_success', 'Profile updated!'); } // Settings page - redirects to channel settings tab public function settings() { return redirect()->route('channel')->with('_open_tab', 'settings'); } // Update settings (password) public function updateSettings(Request $request) { $user = Auth::user(); $request->validate([ 'current_password' => 'required', 'new_password' => 'required|min:8|confirmed', ]); if (! Hash::check($request->current_password, $user->password)) { return back()->withErrors(['current_password' => 'Current password is incorrect']); } $user->update([ 'password' => Hash::make($request->new_password), ]); AuditLog::record('user.password_changed'); return redirect()->route('channel')->with('toast_success', 'Password updated!')->with('_open_tab', 'settings'); } // Logout all other devices public function logoutAllDevices(Request $request) { $request->validate(['password' => 'required']); if (! Hash::check($request->password, Auth::user()->password)) { return back()->withErrors(['logout_password' => 'Incorrect password.'])->with('_open_tab', 'settings'); } Auth::logoutOtherDevices($request->password); // AuthenticateSession stores a hash of the password; logoutOtherDevices rehashes it, // so we must update the session's copy or the current session gets invalidated too. $request->session()->put( 'password_hash_' . Auth::getDefaultDriver(), Auth::user()->getAuthPassword() ); AuditLog::record('user.logout_all'); return redirect()->route('channel')->with('toast_success', 'All other sessions have been logged out.')->with('_open_tab', 'settings'); } // User's channel page - view videos public function channel($username = null) { if ($username) { // Look up by username slug only — never by sequential ID $user = User::where('username', $username)->firstOrFail(); } else { $user = Auth::user(); if (! $user) { return redirect()->route('login'); } $user->channel; // triggers auto-generation if missing } $sort = request('sort', 'latest'); $isOwner = Auth::check() && Auth::user()->id === $user->id; $preview = $isOwner && request()->boolean('preview'); // owner viewing as visitor $isOwner = $isOwner && !$preview; $baseQuery = $isOwner ? Video::where('user_id', $user->id) : Video::public()->where('user_id', $user->id); $allQuery = clone $baseQuery; switch ($sort) { case 'popular': $baseQuery->withCount('viewers')->orderByDesc('viewers_count'); break; case 'oldest': $baseQuery->oldest(); break; default: $baseQuery->latest(); } $videos = $baseQuery->where('is_shorts', false)->get(); $shorts = (clone $allQuery)->where('is_shorts', true)->latest()->get(); $playlists = $isOwner ? $user->playlists()->orderBy('created_at', 'desc')->get() : $user->playlists()->public()->where('is_default', false)->orderBy('created_at', 'desc')->get(); $totalViews = \DB::table('video_views') ->whereIn('video_id', $user->videos()->pluck('id')) ->count(); // Filter social links by visibility for the current viewer $viewer = Auth::user(); $isOwner = $viewer && $viewer->id === $user->id; $isSubscriber = $viewer && !$isOwner && $viewer->isSubscribedTo($user); $socialLinks = $user->socialLinks() ->orderBy('sort_order') ->get() ->filter(function ($link) use ($isOwner, $viewer, $isSubscriber) { if ($isOwner) return true; return match ($link->visibility) { 'public' => true, 'registered' => (bool) $viewer, 'subscribers' => $viewer && $isSubscriber, 'only_me' => false, default => false, }; }); // Posts for the Wall tab $posts = Post::where('user_id', $user->id) ->with(['user', 'video', 'reactions', 'postImages', 'postVideos.video']) ->latest() ->get(); // Horoscope $horoscope = Horoscope::getSign($user->birthday); $viewerSign = null; $compatibility = null; if ($viewer && ! $isOwner && $viewer->birthday) { $viewerSign = Horoscope::getSign($viewer->birthday); $compatibility = Horoscope::compatibility($horoscope, $viewerSign); } $canEdit = $isOwner || (Auth::check() && Auth::user()->isSuperAdmin()); return view('user.channel', compact( 'user', 'videos', 'shorts', 'playlists', 'totalViews', 'sort', 'socialLinks', 'isSubscriber', 'isOwner', 'canEdit', 'posts', 'horoscope', 'viewerSign', 'compatibility', 'preview' )); } // Watch history public function history() { $user = Auth::user(); // Get videos the user has watched, ordered by most recently watched // Include private videos since they are the user's own $videoIds = \DB::table('video_views') ->where('user_id', $user->id) ->orderBy('watched_at', 'desc') ->pluck('video_id') ->unique(); $videos = Video::whereIn('id', $videoIds) ->where(function ($q) use ($user) { $q->where('visibility', '!=', 'private') ->orWhere('user_id', $user->id); }) ->get() ->sortBy(function ($video) use ($videoIds) { return $videoIds->search($video->id); }); return view('user.history', compact('videos')); } // Clear watch history public function clearHistory() { \DB::table('video_views') ->where('user_id', Auth::id()) ->delete(); return redirect()->route('history')->with('toast_success', 'Watch history cleared.'); } // Liked videos public function liked() { $user = Auth::user(); // Include private videos in liked (user's own private videos) $videos = $user->likes() ->where(function ($q) use ($user) { $q->where('visibility', '!=', 'private') ->orWhere('videos.user_id', $user->id); }) ->latest() ->paginate(12); return view('user.liked', compact('videos')); } // Like a video public function like(Video $video) { $user = Auth::user(); if (! $video->isLikedBy($user)) { $video->likes()->attach($user->id); } return back(); } // Unlike a video public function unlike(Video $video) { $user = Auth::user(); $video->likes()->detach($user->id); return back(); } // Toggle like (API) public function toggleLike(Video $video) { $user = Auth::user(); if ($video->isLikedBy($user)) { $video->likes()->detach($user->id); $liked = false; } else { $video->likes()->attach($user->id); $liked = true; } return response()->json([ 'liked' => $liked, 'like_count' => $video->like_count, ]); } public function toggleSubscribe(User $user) { $me = Auth::user(); if ($me->id === $user->id) { return response()->json(['error' => 'Cannot subscribe to yourself'], 422); } if ($me->isSubscribedTo($user)) { $me->subscriptions()->detach($user->id); $subscribed = false; } else { $me->subscriptions()->attach($user->id); $subscribed = true; } return response()->json([ 'subscribed' => $subscribed, 'subscriber_count' => $user->fresh()->subscriber_count, ]); } public function fetchNotifications() { $user = Auth::user(); $rawNotifications = $user->notifications()->latest()->take(50)->get(); // Bulk-fetch current video state for all notification types $videoIds = $rawNotifications ->pluck('data.video_id') ->filter() ->unique() ->values(); $videos = \App\Models\Video::whereIn('id', $videoIds) ->whereIn('visibility', ['public', 'unlisted']) ->get(['id', 'thumbnail', 'visibility']) ->keyBy('id'); $notifications = $rawNotifications ->filter(function ($n) use ($videos) { $videoId = $n->data['video_id'] ?? null; return $videoId && $videos->has($videoId); }) ->take(30) ->map(function ($n) use ($videos) { $data = $n->data; $video = $videos->get($data['video_id']); $data['video_thumbnail'] = $video?->thumbnail ?? null; return [ 'id' => $n->id, 'read' => ! is_null($n->read_at), 'time' => $n->created_at->diffForHumans(), 'data' => $data, ]; }) ->values(); return response()->json([ 'notifications' => $notifications, 'unread_count' => $user->unreadNotifications()->count(), ]); } public function markNotificationRead(string $id) { $notification = Auth::user()->notifications()->findOrFail($id); $notification->markAsRead(); return response()->json(['ok' => true]); } public function markAllNotificationsRead() { Auth::user()->unreadNotifications->markAsRead(); return response()->json(['ok' => true]); } public function updateAvatar(Request $request) { $request->validate(['path' => 'required|string|max:300']); Auth::user()->update(['avatar' => basename($request->path)]); return response()->json(['ok' => true]); } public function updateBanner(Request $request) { $request->validate(['path' => 'required|string|max:300']); Auth::user()->update(['banner' => basename($request->path)]); return response()->json(['ok' => true]); } }