diff --git a/app/Http/Controllers/Backend/User/DashboardController.php b/app/Http/Controllers/Backend/User/DashboardController.php index adc7b863e..cfcca456e 100644 --- a/app/Http/Controllers/Backend/User/DashboardController.php +++ b/app/Http/Controllers/Backend/User/DashboardController.php @@ -6,89 +6,103 @@ use App\Http\Controllers\Controller; use App\Models\Status; use App\Models\User; -use Carbon\Carbon; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder; abstract class DashboardController extends Controller { + private static function getGenericQuery(User $user): Builder { + $query = Status::with([ + 'event', + 'likes', + 'user.blockedByUsers', + 'user.blockedUsers', + 'checkin', + 'tags', + 'mentions.mentioned', + 'checkin.originStopover.station', + 'checkin.destinationStopover.station', + 'checkin.trip.stopovers.station' + ]) + ->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id') + ->join('train_stopovers AS origin_stopover', 'train_checkins.origin_stopover_id', '=', 'origin_stopover.id') + ->join('users', 'statuses.user_id', '=', 'users.id') + ->where('origin_stopover.departure_real', '<', now()->addMinutes(20)) + ->where('origin_stopover.departure_real', '>', now()->subDays(2)) //TODO: discuss - dashboard should show statuses from the last 2 days. This is a performance dealbreaker + ->select('statuses.*') + ->orderByDesc('origin_stopover.departure_real'); // TODO: manual_departure + + // left join follows to check if user follows the status author (checked in caller function) + $query->leftJoin('follows', function($join) use ($user) { + $join->on('follows.follow_id', '=', 'users.id') + ->where('follows.user_id', '=', $user->id); + }); + + return $query; + } + public static function getPrivateDashboard(User $user): Paginator { - $followingIDs = $user->follows->pluck('id'); - $followingIDs[] = $user->id; - return Status::with([ - 'event', - 'likes', - 'user.blockedByUsers', - 'user.blockedUsers', - 'checkin', - 'tags', - 'mentions.mentioned', - 'checkin.originStopover.station', - 'checkin.destinationStopover.station', - 'checkin.trip.stopovers.station' - ]) - ->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id') - ->select('statuses.*') - ->where('train_checkins.departure', '<', Carbon::now()->addMinutes(20)) - ->orderBy('train_checkins.departure', 'desc') - ->whereIn('statuses.user_id', $followingIDs) - ->whereNotIn('statuses.user_id', $user->mutedUsers->pluck('id')) + $query = self::getGenericQuery($user); + + return $query->whereNotNull('follows.id') ->whereIn('statuses.visibility', [ StatusVisibility::PUBLIC->value, StatusVisibility::FOLLOWERS->value, StatusVisibility::AUTHENTICATED->value ]) ->orWhere('statuses.user_id', $user->id) - ->latest() ->simplePaginate(15); } public static function getGlobalDashboard(User $user): Paginator { - return Status::with([ - 'event', - 'likes', - 'user.blockedByUsers', - 'user.blockedUsers', - 'checkin', - 'mentions.mentioned', - 'tags', - 'checkin.originStopover.station', - 'checkin.destinationStopover.station', - 'checkin.trip.stopovers.station' - ]) - ->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id') - ->join('users', 'statuses.user_id', '=', 'users.id') - ->where(function(Builder $query) use ($user) { - //Visibility checks: One of the following options must be true - - //Option 1: User is public AND status is public - $query->where(function(Builder $query) { - $query->where('users.private_profile', 0) - ->whereIn('visibility', [ - StatusVisibility::PUBLIC->value, - StatusVisibility::AUTHENTICATED->value - ]); - }); - - //Option 2: Status is from oneself - $query->orWhere('users.id', $user->id); - - //Option 3: Status is from a followed BUT not unlisted or private - $query->orWhere(function(Builder $query) use ($user) { - $query->whereIn('users.id', $user->follows()->select('follow_id')) - ->whereNotIn('statuses.visibility', [ - StatusVisibility::UNLISTED->value, - StatusVisibility::PRIVATE->value, - ]); - }); - }) - ->where('train_checkins.departure', '<', Carbon::now()->addMinutes(20)) - ->whereNotIn('statuses.user_id', $user->mutedUsers()->select('muted_id')) - ->whereNotIn('statuses.user_id', $user->blockedUsers()->select('blocked_id')) - ->whereNotIn('statuses.user_id', $user->blockedByUsers()->select('user_id')) - ->select('statuses.*') - ->orderByDesc('train_checkins.departure') - ->simplePaginate(15); + $query = self::getGenericQuery($user); + + // exclude muted users + $query->leftJoin('user_mutes', function($join) use ($user) { + $join->on('user_mutes.muted_id', '=', 'users.id') + ->where('user_mutes.user_id', '=', $user->id); + })->whereNull('user_mutes.id'); + + // exclude blocked users + $query->leftJoin('user_blocks AS blocked_users', function($join) use ($user) { + $join->on('blocked_users.blocked_id', '=', 'users.id') + ->where('blocked_users.user_id', '=', $user->id); + })->whereNull('blocked_users.id'); + + // exclude blocked by users + $query->leftJoin('user_blocks AS blocked_by_users', function($join) use ($user) { + $join->on('blocked_by_users.user_id', '=', 'users.id') + ->where('blocked_by_users.blocked_id', '=', $user->id); + })->whereNull('blocked_by_users.id'); + + // only show statuses user is allowed to see + $query->where(function(Builder $query) use ($user) { + //Visibility checks: One of the following options must be true + + //Option 1: User is public AND status is public + $query->where(function(Builder $query) { + $query->where('users.private_profile', 0) + ->whereIn('visibility', [ + StatusVisibility::PUBLIC->value, + StatusVisibility::AUTHENTICATED->value + ]); + }); + + //Option 2: Status is from oneself + $query->orWhere('users.id', $user->id); + + //Option 3: Status is from a followed BUT not unlisted or private + $query->orWhere(function(Builder $query) { + // see join above + $query->whereNotNull('follows.id') + ->whereNotIn('statuses.visibility', [ + StatusVisibility::UNLISTED->value, + StatusVisibility::PRIVATE->value, + ]); + }); + }); + + return $query->simplePaginate(15); } } diff --git a/database/migrations/2024_06_24_000000_add_status_id_departure_index_to_checkins.php b/database/migrations/2024_06_24_000000_add_status_id_departure_index_to_checkins.php new file mode 100644 index 000000000..471e0eff7 --- /dev/null +++ b/database/migrations/2024_06_24_000000_add_status_id_departure_index_to_checkins.php @@ -0,0 +1,20 @@ +index(['status_id', 'departure']); + }); + } + + public function down(): void { + Schema::table('train_checkins', static function(Blueprint $table) { + $table->dropIndex(['status_id', 'departure']); + }); + } +}; diff --git a/database/migrations/2024_06_24_000001_add_id_departure_real_departure_planned_index_to_train_stopovers.php b/database/migrations/2024_06_24_000001_add_id_departure_real_departure_planned_index_to_train_stopovers.php new file mode 100644 index 000000000..393271718 --- /dev/null +++ b/database/migrations/2024_06_24_000001_add_id_departure_real_departure_planned_index_to_train_stopovers.php @@ -0,0 +1,20 @@ +index(['id', 'departure_real', 'departure_planned']); + }); + } + + public function down(): void { + Schema::table('train_stopovers', static function(Blueprint $table) { + $table->dropIndex(['id', 'departure_real', 'departure_planned']); + }); + } +}; diff --git a/database/migrations/2024_06_24_000002_add_id_private_profile_index_to_users.php b/database/migrations/2024_06_24_000002_add_id_private_profile_index_to_users.php new file mode 100644 index 000000000..c3fed5a35 --- /dev/null +++ b/database/migrations/2024_06_24_000002_add_id_private_profile_index_to_users.php @@ -0,0 +1,21 @@ +index(['id', 'private_profile']); + }); + } + + public function down(): void { + Schema::table('users', static function(Blueprint $table) { + $table->dropIndex(['id', 'private_profile']); + }); + } +}; diff --git a/database/migrations/2024_06_24_000003_add_follow_id_user_id_index_to_follows.php b/database/migrations/2024_06_24_000003_add_follow_id_user_id_index_to_follows.php new file mode 100644 index 000000000..08dd02986 --- /dev/null +++ b/database/migrations/2024_06_24_000003_add_follow_id_user_id_index_to_follows.php @@ -0,0 +1,20 @@ +index(['follow_id', 'user_id']); + }); + } + + public function down(): void { + Schema::table('follows', static function(Blueprint $table) { + $table->dropIndex(['follow_id', 'user_id']); + }); + } +}; diff --git a/database/seeders/UsersTableSeeder.php b/database/seeders/UsersTableSeeder.php index 38ecc31dd..ded374d6c 100644 --- a/database/seeders/UsersTableSeeder.php +++ b/database/seeders/UsersTableSeeder.php @@ -5,7 +5,6 @@ use App\Models\IcsToken; use App\Models\User; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\Hash; class UsersTableSeeder extends Seeder { @@ -23,7 +22,6 @@ public function run(): void { 'username' => 'Gertrud123', 'name' => 'Gertrud', 'email' => 'gertrud@traewelling.de', - 'password' => Hash::make('thisisnotasecurepassword123'), ]); $gertrud->assignRole('admin'); $gertrud->assignRole('closed-beta'); @@ -37,7 +35,6 @@ public function run(): void { 'avatar' => null, // no avatar 'email' => 'bob@example.com', 'private_profile' => true, - 'password' => Hash::make('thisisnotasecurepassword123') ]); } diff --git a/docs/contributing/dev-setup.md b/docs/contributing/dev-setup.md index 5c90c6b0c..52e8b8cfe 100644 --- a/docs/contributing/dev-setup.md +++ b/docs/contributing/dev-setup.md @@ -52,6 +52,11 @@ php artisan passport:install Use your webserver of choice or the in php included dev server (`php artisan serve`) to boot the application. You should see the Träwelling homepage at http://localhost:8000. +If you have seeded the database, you can log in using the following credentials: + +- Username: `Gertrud123` or `bob` +- Password: `password` + Additionally, for continuous functionality: - Create a cron job to run `php artisan schedule:run` every minute.