Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
48eb728
New translations for "Hide for User" feature
VXGP Dec 19, 2025
f94e858
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
7f64cf6
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
b10158c
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
11692c7
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
931c483
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
4cc3c27
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
b16bf71
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
639fe23
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
785b10e
"Hide Check-In for User" - Feature
VXGP Dec 19, 2025
ea86d4c
Database Migration Fix
VXGP Dec 20, 2025
b5141f8
Missing Database Migration
VXGP Dec 21, 2025
fe42423
Merge branch 'develop' into develop
VXGP Dec 21, 2025
e51a83f
Fixed "Not up to quality standards"
VXGP Dec 23, 2025
93ea0e3
Fixed "Not up to quality standards"
VXGP Dec 23, 2025
bd078b2
Fixed "Not up to quality standards"
VXGP Dec 23, 2025
ee56108
Merge branch 'develop' into develop
VXGP Dec 23, 2025
f6423d6
Merge branch 'develop' into develop
VXGP Dec 26, 2025
315343b
Improved the Hide-Users button design
VXGP Dec 26, 2025
5d7199b
Changed button icon and added option on check-in
VXGP Dec 26, 2025
18f7b43
Added option to hide users directly at the check-in
VXGP Dec 26, 2025
a2289e2
Added option to hide users directly at the check-in
VXGP Dec 26, 2025
87c62be
Added option to hide users directly at the check-in
VXGP Dec 26, 2025
a897a6a
Added option to hide users directly at the check-in
VXGP Dec 27, 2025
d6a05d1
Added option to hide users directly at the check-in
VXGP Dec 27, 2025
1a85304
Fixed id/uuid error
VXGP Dec 27, 2025
9d68f28
Improved button design and fixed hover
VXGP Dec 27, 2025
090f0f7
Fixed a bug
VXGP Dec 28, 2025
d13614c
Merge branch 'develop' into develop
VXGP Dec 28, 2025
c75a016
Fixed "Not up to quality standards"
VXGP Dec 28, 2025
8dec52e
Fixed "Not up to quality standards" again
VXGP Dec 28, 2025
3590615
Fixed "Not up to quality standards"
VXGP Dec 28, 2025
7f1ac3e
Fixed "Not up to quality standards"
VXGP Jan 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/Dto/Internal/CheckInRequestDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CheckInRequestDto
public bool $forceFlag;
public bool $postOnMastodonFlag;
public bool $chainFlag;
public array $hiddenUserIds;

public function __construct() {
$this->travelReason = Business::PRIVATE;
Expand All @@ -36,6 +37,7 @@ public function __construct() {
$this->forceFlag = false;
$this->postOnMastodonFlag = false;
$this->chainFlag = false;
$this->hiddenUserIds = [];
}

public function setUser(Authenticatable $user): CheckInRequestDto {
Expand Down Expand Up @@ -102,4 +104,9 @@ public function setChainFlag(bool $chainFlag): CheckInRequestDto {
$this->chainFlag = $chainFlag;
return $this;
}

public function setHiddenUserIds(array $hiddenUserIds): CheckInRequestDto {
$this->hiddenUserIds = $hiddenUserIds;
return $this;
}
}
210 changes: 210 additions & 0 deletions app/Http/Controllers/API/v1/StatusHiddenUserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

namespace App\Http\Controllers\API\v1;

use App\Http\Resources\UserResource;
use App\Models\Status;
use App\Models\StatusHiddenUser;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class StatusHiddenUserController extends Controller
{
/**
* @OA\Get(
* path="/status/{statusId}/hidden-users",
* operationId="getHiddenUsers",
* tags={"Status"},
* summary="Get users hidden from viewing a status",
* description="Returns list of users who are hidden from viewing this specific status",
* @OA\Parameter (
* name="statusId",
* in="path",
* description="Status-ID",
* example=1337,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="successful operation",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/UserResource")
* )
* )
* ),
* @OA\Response(response=403, description="User not authorized"),
* @OA\Response(response=404, description="Status not found"),
* security={
* {"passport": {"read-statuses"}}, {"token": {}}
* }
* )
*
* @param int $statusId
* @return JsonResponse
*/
public function index(int $statusId): JsonResponse {
try {
$status = Status::findOrFail($statusId);
$this->authorize('update', $status);

$hiddenUsers = $status->hiddenUsers()->with('user')->get()->pluck('user');

return $this->sendResponse(UserResource::collection($hiddenUsers));
} catch (ModelNotFoundException) {
return $this->sendError('Status not found', 404);
} catch (AuthorizationException) {
return $this->sendError('You are not authorized to view hidden users for this status', 403);
}
}

/**
* @OA\Post(
* path="/status/{statusId}/hidden-users",
* operationId="addHiddenUser",
* tags={"Status"},
* summary="Add a user to the hidden list for a status",
* description="Adds a user who will not be able to see this specific status",
* @OA\Parameter (
* name="statusId",
* in="path",
* description="Status-ID",
* example=1337,
* @OA\Schema(type="integer")
* ),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"userId"},
* @OA\Property(property="userId", type="integer", example=42,
* description="ID of user to hide this status from")
* )
* ),
* @OA\Response(
* response=201,
* description="User successfully added to hidden list",
* @OA\JsonContent(
* @OA\Property(property="message", type="string", example="User added to hidden list")
* )
* ),
* @OA\Response(response=400, description="Bad request"),
* @OA\Response(response=403, description="User not authorized"),
* @OA\Response(response=404, description="Status or user not found"),
* @OA\Response(response=409, description="User already hidden"),
* security={
* {"passport": {"write-statuses"}}, {"token": {}}
* }
* )
*
* @param Request $request
* @param int $statusId
* @return JsonResponse
* @throws ValidationException
*/
public function store(Request $request, int $statusId): JsonResponse {
$validator = Validator::make($request->all(), [
'userId' => ['required', 'integer', 'exists:users,id'],
]);

if ($validator->fails()) {
return $this->sendError($validator->errors(), 400);
}

$validated = $validator->validate();

try {
$status = Status::findOrFail($statusId);
$this->authorize('update', $status);

$userToHide = User::findOrFail($validated['userId']);

// Can't hide yourself from your own status
if ($userToHide->id === $status->user_id) {
return $this->sendError('You cannot hide yourself from your own status', 400);
}

// Check if already hidden
if ($status->hiddenUsers()->where('user_id', $userToHide->id)->exists()) {
return $this->sendError('User is already hidden from this status', 409);
}

StatusHiddenUser::create([
'status_id' => $status->id,
'user_id' => $userToHide->id,
]);

return $this->sendResponse(['message' => __('status.hidden-user.added')], 201);
} catch (ModelNotFoundException) {
return $this->sendError('Status or user not found', 404);
} catch (AuthorizationException) {
return $this->sendError('You are not authorized to modify hidden users for this status', 403);
}
}

/**
* @OA\Delete(
* path="/status/{statusId}/hidden-users/{userId}",
* operationId="removeHiddenUser",
* tags={"Status"},
* summary="Remove a user from the hidden list for a status",
* description="Removes a user from the hidden list, allowing them to see this status again",
* @OA\Parameter (
* name="statusId",
* in="path",
* description="Status-ID",
* example=1337,
* @OA\Schema(type="integer")
* ),
* @OA\Parameter (
* name="userId",
* in="path",
* description="User-ID to remove from hidden list",
* example=42,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="User successfully removed from hidden list",
* @OA\JsonContent(
* @OA\Property(property="message", type="string", example="User removed from hidden list")
* )
* ),
* @OA\Response(response=403, description="User not authorized"),
* @OA\Response(response=404, description="Status or hidden user entry not found"),
* security={
* {"passport": {"write-statuses"}}, {"token": {}}
* }
* )
*
* @param int $statusId
* @param int $userId
* @return JsonResponse
*/
public function destroy(int $statusId, int $userId): JsonResponse {
try {
$status = Status::findOrFail($statusId);
$this->authorize('update', $status);

$hiddenEntry = $status->hiddenUsers()->where('user_id', $userId)->first();

if (!$hiddenEntry) {
return $this->sendError('User is not hidden from this status', 404);
}

$hiddenEntry->delete();

return $this->sendResponse(['message' => __('status.hidden-user.removed')]);
} catch (ModelNotFoundException) {
return $this->sendError('Status not found', 404);
} catch (AuthorizationException) {
return $this->sendError('You are not authorized to modify hidden users for this status', 403);
}
}
}
38 changes: 23 additions & 15 deletions app/Http/Controllers/API/v1/TransportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,22 +393,30 @@ public function create(Request $request): JsonResponse {

$withUsers = null;
$validated = $request->validate([
'body' => ['nullable', 'max:280'],
'business' => ['nullable', new Enum(Business::class)],
'visibility' => ['nullable', new Enum(StatusVisibility::class)],
'eventId' => ['nullable', 'integer', 'exists:events,id'],
'toot' => ['nullable', 'boolean'],
'chainPost' => ['nullable', 'boolean'],
'ibnr' => ['nullable', 'boolean'],
'tripId' => ['required'],
'lineName' => ['required'],
'start' => ['required', 'numeric'],
'destination' => ['required', 'numeric'],
'departure' => ['required', 'date'],
'arrival' => ['required', 'date'],
'force' => ['nullable', 'boolean'],
'with' => ['nullable', 'array', 'max:10'],
'body' => ['nullable', 'max:280'],
'business' => ['nullable', new Enum(Business::class)],
'visibility' => ['nullable', new Enum(StatusVisibility::class)],
'eventId' => ['nullable', 'integer', 'exists:events,id'],
'toot' => ['nullable', 'boolean'],
'chainPost' => ['nullable', 'boolean'],
'ibnr' => ['nullable', 'boolean'],
'tripId' => ['required'],
'lineName' => ['required'],
'start' => ['required', 'numeric'],
'destination' => ['required', 'numeric'],
'departure' => ['required', 'date'],
'arrival' => ['required', 'date'],
'force' => ['nullable', 'boolean'],
'with' => ['nullable', 'array', 'max:10'],
'hiddenUserIds' => ['nullable', 'array'],
'hiddenUserIds.*' => ['integer', 'exists:users,id'],
]);

// Validate that user is not trying to hide themselves
if (isset($validated['hiddenUserIds']) && in_array(Auth::id(), $validated['hiddenUserIds'])) {
return $this->sendError('You cannot hide yourself from your own status.', 400);
}

if (isset($validated['with'])) {
$withUsers = User::whereIn('id', $validated['with'])->get();
$forbiddenUsers = collect();
Expand Down
74 changes: 42 additions & 32 deletions app/Http/Controllers/Backend/Transport/StatusController.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,42 +56,52 @@ public static function getPrintableEscapedBody(Status $status): string {
*/
public static function filterStatusVisibility(?User $viewingUser = null): Closure {
return function(EloquentBuilder $query) use ($viewingUser) {
//Visibility checks: One of the following options must be true

//Option 1: User is public AND status is public
$query->where(function(EloquentBuilder $query) use ($viewingUser) {
$query->where('users.private_profile', 0)
->whereIn('visibility', [StatusVisibility::PUBLIC->value] + ($viewingUser !== null ? [StatusVisibility::AUTHENTICATED->value] : []));
});

if ($viewingUser !== null) {
//Option 2: Status is from oneself
$query->orWhere('users.id', $viewingUser->id);

//Option 3: Status is from a followed BUT not unlisted or private or trusted users only
$query->orWhere(function(EloquentBuilder $query) use ($viewingUser) {
$query->whereIn('users.id', $viewingUser->follows()->select('follow_id'))
->whereNotIn('statuses.visibility', [
StatusVisibility::UNLISTED->value,
StatusVisibility::PRIVATE->value,
StatusVisibility::TRUSTED->value,
]);
$query->whereNotExists(function(QueryBuilder $subQuery) use ($viewingUser) {
$subQuery->select(DB::raw(1))
->from('status_hidden_users')
->whereColumn('status_hidden_users.status_id', 'statuses.id')
->where('status_hidden_users.user_id', $viewingUser->id);
});
}

//Option 4: Status is from a user who trusts the viewing user
$query->orWhere(function(EloquentBuilder $query) use ($viewingUser) {
$query->where('statuses.visibility', StatusVisibility::TRUSTED->value)
->whereExists(function(QueryBuilder $subQuery) use ($viewingUser) {
$subQuery->from('trusted_users')
->whereColumn('trusted_users.user_id', 'statuses.user_id')
->where('trusted_users.trusted_id', $viewingUser->id)
->where(function(QueryBuilder $expireQuery) {
$expireQuery->whereNull('trusted_users.expires_at')
->orWhere('trusted_users.expires_at', '>', now());
});
});
//Visibility checks: One of the following options must be true
$query->where(function(EloquentBuilder $visibility) use ($viewingUser) {
//Option 1: User is public AND status is public
$visibility->where(function(EloquentBuilder $query) use ($viewingUser) {
$query->where('users.private_profile', 0)
->whereIn('visibility', [StatusVisibility::PUBLIC->value] + ($viewingUser !== null ? [StatusVisibility::AUTHENTICATED->value] : []));
});
}

if ($viewingUser !== null) {
//Option 2: Status is from oneself
$visibility->orWhere('users.id', $viewingUser->id);

//Option 3: Status is from a followed BUT not unlisted or private or trusted users only
$visibility->orWhere(function(EloquentBuilder $query) use ($viewingUser) {
$query->whereIn('users.id', $viewingUser->follows()->select('follow_id'))
->whereNotIn('statuses.visibility', [
StatusVisibility::UNLISTED->value,
StatusVisibility::PRIVATE->value,
StatusVisibility::TRUSTED->value,
]);
});

//Option 4: Status is from a user who trusts the viewing user
$visibility->orWhere(function(EloquentBuilder $query) use ($viewingUser) {
$query->where('statuses.visibility', StatusVisibility::TRUSTED->value)
->whereExists(function(QueryBuilder $subQuery) use ($viewingUser) {
$subQuery->from('trusted_users')
->whereColumn('trusted_users.user_id', 'statuses.user_id')
->where('trusted_users.trusted_id', $viewingUser->id)
->where(function(QueryBuilder $expireQuery) {
$expireQuery->whereNull('trusted_users.expires_at')
->orWhere('trusted_users.expires_at', '>', now());
});
});
});
}
});
};
}
}
10 changes: 10 additions & 0 deletions app/Http/Controllers/Backend/Transport/TrainCheckinController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public static function checkin(CheckInRequestDto $dto, ?User $checkedInBy = null
event: $dto->event
);

// Add hidden users if specified
if (!empty($dto->hiddenUserIds)) {
foreach ($dto->hiddenUserIds as $userId) {
\App\Models\StatusHiddenUser::create([
'status_id' => $status->id,
'user_id' => $userId,
]);
}
}

$checkinResponse = self::createCheckin(
status: $status,
trip: $dto->trip,
Expand Down
Loading