From 92dfdeda11383ff8eed64f35ef85e0ca476fcf1c Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Sun, 20 Sep 2026 22:02:04 -0600 Subject: [PATCH 01/10] Render boosts in clients that only read the top level --- app/Http/Controllers/Api/ApiV1Controller.php | 6 ++++-- app/Services/StatusService.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 6bbc1d5e0..0283735eb 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2786,7 +2786,8 @@ class ApiV1Controller extends Controller return false; } - $status['account'] = $account; + // Not $status['account'] = $account: $account is resolved from + // the row, StatusService picks the right one per client if ($pid) { $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); @@ -2857,7 +2858,8 @@ class ApiV1Controller extends Controller return false; } - $status['account'] = $account; + // Not $status['account'] = $account: $account is resolved from + // the row, StatusService picks the right one per client if ($pid) { $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index d14d84c75..cf20b6b30 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -90,6 +90,20 @@ class StatusService unset($res['_pid']); } + // A client that resolves reblogs declares include_reblogs; one that + // only reads the top level renders a boost as an empty card, so give it + // the shared status there instead. + if (! $mastodonMode + && request()->has('_pe') + && ! request()->filled('include_reblogs') + && ! empty($res['reblog'])) { + foreach (['account', 'content', 'content_text', 'emojis', 'media_attachments'] as $key) { + if (array_key_exists($key, $res['reblog'])) { + $res[$key] = $res['reblog'][$key]; + } + } + } + return $res; } From 24259fa48949cc22d07df5a0aefffad8cc469d73 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Sun, 20 Sep 2026 22:03:16 -0600 Subject: [PATCH 02/10] Honour photo_reblogs_only in the home timeline --- app/Http/Controllers/Api/ApiV1Controller.php | 36 ++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 6bbc1d5e0..6c670f6a0 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2642,6 +2642,16 @@ class ApiV1Controller extends Controller $inTypes = $includeReblogs ? ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + + // "Photo reblogs only" + $photosReblogsOnly = $request->filled('photos_reblogs_only') + ? $request->boolean('photos_reblogs_only') + : data_get($other, 'photo_reblogs_only', false); + $reblogTargetTypes = array_diff($inTypes, ['share']); + + // Filtering happens after the fetch, so fetch deeper to fill a page + $fetchLimit = $photosReblogsOnly ? $limit * 6 : $limit * 2; + AccountService::setLastActive($request->user()->id); $cachedFilters = CustomFilter::getCachedFiltersForAccount($pid); @@ -2770,7 +2780,7 @@ class ApiV1Controller extends Controller ->whereIn('type', $inTypes) ->whereIn('visibility', ['public', 'unlisted', 'private']) ->orderByDesc('id') - ->take(($limit * 2)) + ->take($fetchLimit) ->get() ->map(function ($s) use ($pid, $napi) { try { @@ -2796,8 +2806,15 @@ class ApiV1Controller extends Controller return $status; }) - ->filter(function ($status) { - return $status && isset($status['account']); + ->filter(function ($status) use ($photosReblogsOnly, $reblogTargetTypes) { + if (! $status || ! isset($status['account'])) { + return false; + } + + // direct posts pass; a boost must share a photo or video + return ! $photosReblogsOnly + || empty($status['reblog']) + || in_array(data_get($status['reblog'], 'pf_type'), $reblogTargetTypes); }) ->map(function ($status) use ($pid) { if (! empty($status['reblog'])) { @@ -2841,7 +2858,7 @@ class ApiV1Controller extends Controller ->whereIn('type', $inTypes) ->whereIn('visibility', ['public', 'unlisted', 'private']) ->orderByDesc('id') - ->take(($limit * 2)) + ->take($fetchLimit) ->get() ->map(function ($s) use ($pid, $napi) { try { @@ -2867,8 +2884,15 @@ class ApiV1Controller extends Controller return $status; }) - ->filter(function ($status) { - return $status && isset($status['account']); + ->filter(function ($status) use ($photosReblogsOnly, $reblogTargetTypes) { + if (! $status || ! isset($status['account'])) { + return false; + } + + // direct posts pass; a boost must share a photo or video + return ! $photosReblogsOnly + || empty($status['reblog']) + || in_array(data_get($status['reblog'], 'pf_type'), $reblogTargetTypes); }) ->map(function ($status) use ($pid) { if (! empty($status['reblog'])) { From c381362b3a0c289a68296f37454cb89e577ef396 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 12:15:56 -0600 Subject: [PATCH 03/10] Fix Postgres backup connection and statuses nullability --- config/backup.php | 17 +++++++++- ...94847_fix_non_nullable_postgres_errors.php | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php diff --git a/config/backup.php b/config/backup.php index 3e9aff72f..a535a7b17 100644 --- a/config/backup.php +++ b/config/backup.php @@ -77,8 +77,23 @@ return [ ], ], + 'mariadb' => [ + 'dump' => [ + 'useSingleTransaction' => true, + 'useQuick' => true, + ], + ], + + 'pgsql' => [ + 'dump' => [ + 'useSingleTransaction' => true, + 'useQuick' => true, + ], + ], + + // Hardcoding 'mysql' breaks backup:run on non-MySQL installs 'databases' => [ - 'mysql', + env('DB_CONNECTION', 'mysql'), ], ], diff --git a/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php b/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php new file mode 100644 index 000000000..8eb0f43c6 --- /dev/null +++ b/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php @@ -0,0 +1,32 @@ +change()` without `->nullable()`, making both columns NOT NULL. + * DirectMessageController still writes null into caption, which errors + * on PostgreSQL. + */ + public function up(): void + { + Schema::table('statuses', function ($table) { + $table->text('caption')->nullable()->change(); + $table->text('rendered')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; From 1fc25d067a7d7fd87052a57d64f4e04c59113819 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 13:19:14 -0600 Subject: [PATCH 04/10] Fix the vips image driver in web SAPIs --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8b9eeeab0..13f37f47b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -127,6 +127,8 @@ RUN install-php-extensions \ vips \ ffi +RUN printf 'ffi.enable=1\n' > /usr/local/etc/php/conf.d/zz-ffi-enable.ini + COPY --from=ffmpeg /usr/local/ffmpeg/bin/ffmpeg /usr/bin/ffmpeg COPY --from=ffmpeg /usr/local/ffmpeg/bin/ffprobe /usr/bin/ffprobe COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib From 0e6459ddb95e5ff8a03369c75c43a03148126363 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 15:46:33 -0600 Subject: [PATCH 05/10] Preserve the original HTML for remote status content --- app/Transformer/Api/StatusStatelessTransformer.php | 5 ++++- app/Transformer/Api/StatusTransformer.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php index 847f511a8..f6a3d92ca 100644 --- a/app/Transformer/Api/StatusStatelessTransformer.php +++ b/app/Transformer/Api/StatusStatelessTransformer.php @@ -22,7 +22,10 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract { $taggedPeople = MediaTagService::get($status->id); $poll = $status->type === 'poll' ? PollService::get($status->id) : null; - $rendered = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ''; + // Remote posts keep the HTML they arrived with, so the link targets survive + $rendered = $status->local || ! $status->rendered + ? ($status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '') + : $status->rendered; return [ '_v' => 1, diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 97493599b..5ffa99434 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -24,7 +24,10 @@ class StatusTransformer extends Fractal\TransformerAbstract $pid = request()->user()->profile_id; $taggedPeople = MediaTagService::get($status->id); $poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null; - $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ''; + // Remote posts keep the HTML they arrived with, so the link targets survive + $content = $status->local || ! $status->rendered + ? ($status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '') + : $status->rendered; return [ '_v' => 1, From 9c0a87e95080279062e3ee6c0718afb21d239af7 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 15:51:28 -0600 Subject: [PATCH 06/10] Select rendered in the comment and profile queries --- app/Http/Controllers/InternalApiController.php | 1 + app/Http/Controllers/PublicApiController.php | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/InternalApiController.php b/app/Http/Controllers/InternalApiController.php index 96be90edb..2e5f453bf 100644 --- a/app/Http/Controllers/InternalApiController.php +++ b/app/Http/Controllers/InternalApiController.php @@ -328,6 +328,7 @@ class InternalApiController extends Controller 'id', 'uri', 'caption', + 'rendered', 'profile_id', 'type', 'in_reply_to_id', diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 40e0ac2c4..8f8296894 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -179,7 +179,7 @@ class PublicApiController extends Controller $replies = $status->comments() ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->select('id', 'caption', 'rendered', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '>=', $request->min_id) ->orderBy('id', 'desc') ->paginate($limit); @@ -188,7 +188,7 @@ class PublicApiController extends Controller $replies = $status->comments() ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->select('id', 'caption', 'rendered', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '<=', $request->max_id) ->orderBy('id', 'desc') ->paginate($limit); @@ -197,7 +197,7 @@ class PublicApiController extends Controller $replies = Status::whereInReplyToId($status->id) ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->select('id', 'caption', 'rendered', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->orderBy('id', 'desc') ->paginate($limit); } From cde0613f82a5b2e71859b5ba77f3fc03140593f0 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 16:41:41 -0600 Subject: [PATCH 07/10] Preserve the original HTML in search results --- app/Http/Controllers/SearchController.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index e8b12ec3d..d961fde8a 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -340,7 +340,10 @@ class SearchController extends Controller if ($media) { $url = $media->remote_url; } - $content = $item->caption ? Autolink::create()->autolink($item->caption) : null; + // Remote posts keep the HTML they arrived with, so the link targets survive + $content = $item->local || ! $item->rendered + ? ($item->caption ? Autolink::create()->autolink($item->caption) : null) + : $item->rendered; $this->tokens['posts'] = [[ 'count' => 0, 'url' => "/i/web/post/_/$item->profile_id/$item->id", @@ -364,7 +367,10 @@ class SearchController extends Controller if ($media) { $url = $media->remote_url; } - $content = $item->caption ? Autolink::create()->autolink($item->caption) : null; + // Remote posts keep the HTML they arrived with, so the link targets survive + $content = $item->local || ! $item->rendered + ? ($item->caption ? Autolink::create()->autolink($item->caption) : null) + : $item->rendered; $this->tokens['posts'] = [[ 'count' => 0, 'url' => "/i/web/post/_/$item->profile_id/$item->id", From de15fb57d914c0e53595e538b28a9fbd127ba8ce Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 17:30:23 -0600 Subject: [PATCH 08/10] Return favourited as a boolean in Mastodon mode --- app/Services/StatusService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index d14d84c75..4e8157f1e 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -153,6 +153,7 @@ class StatusService MediaService::getMastodon($status['id']) ); + $status['favourited'] = false; $status['muted'] = false; $status['reblogged'] = false; From f9611a6704f5de2b967e00e29e3d41a732f78f77 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Mon, 21 Sep 2026 10:55:06 -0600 Subject: [PATCH 09/10] Give the nested boost target Mastodon field names --- app/Services/StatusService.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index d14d84c75..e155b0577 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -115,6 +115,14 @@ class StatusService return $status; } + // The shared status nests under `reblog` and needs Mastodon field names too + if (! empty($status['reblog'])) { + $status['reblog']['replies_count'] = $status['reblog']['reply_count'] ?? 0; + $status['reblog']['favourited'] = false; + $status['reblog']['muted'] = false; + $status['reblog']['reblogged'] = false; + } + unset( $status['_v'], $status['comments_disabled'], From d86ef9ae520380222b794de3022037004367b382 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 21 Sep 2026 20:39:14 -0600 Subject: [PATCH 10/10] Update 2025_02_10_194847_fix_non_nullable_postgres_errors.php --- .../2025_02_10_194847_fix_non_nullable_postgres_errors.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php b/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php index 8eb0f43c6..9a33ace08 100644 --- a/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php +++ b/database/migrations/2025_02_10_194847_fix_non_nullable_postgres_errors.php @@ -1,7 +1,6 @@ text('caption')->nullable()->change(); $table->text('rendered')->nullable()->change(); });