diff --git a/public/images/accounts/discord.png b/public/images/accounts/discord.png new file mode 100644 index 00000000..92b7bde1 Binary files /dev/null and b/public/images/accounts/discord.png differ diff --git a/resources/js/components/posts/editor/DiscordSettings.vue b/resources/js/components/posts/editor/DiscordSettings.vue index 45141a0d..31a8c7e3 100644 --- a/resources/js/components/posts/editor/DiscordSettings.vue +++ b/resources/js/components/posts/editor/DiscordSettings.vue @@ -87,6 +87,16 @@ const channelId = computed({ set: (value: string) => updateMeta({ channel_id: value || null }), }); +// Keep a saved channel selectable even before the live list loads (or if the +// lookup is unavailable), so editing a post never visually "loses" its channel. +const channelOptions = computed(() => { + if (channelId.value && !channels.value.some((channel) => channel.id === channelId.value)) { + return [{ id: channelId.value, name: channelId.value }, ...channels.value]; + } + + return channels.value; +}); + const errors = usePageErrors(); const channelError = computed(() => { if (props.meta?.channel_id) { @@ -202,7 +212,7 @@ const updateEmbed = (index: number, patch: Partial) => - + diff --git a/tests/Feature/Services/Social/DiscordPublisherTest.php b/tests/Feature/Services/Social/DiscordPublisherTest.php index 34b77609..32bf2845 100644 --- a/tests/Feature/Services/Social/DiscordPublisherTest.php +++ b/tests/Feature/Services/Social/DiscordPublisherTest.php @@ -11,6 +11,7 @@ use App\Models\SocialAccount; use App\Models\User; use App\Models\Workspace; +use App\Services\Media\MediaOptimizer; use App\Services\Social\Discord\DiscordPublisher; use Illuminate\Support\Facades\Http; @@ -120,6 +121,41 @@ function fakeDiscord(array $messageResponse = ['id' => '777'], int $status = 200 }); }); +test('uploads media as a multipart attachment', function () { + $this->post->update([ + 'media' => [[ + 'id' => 'm1', + 'path' => 'media/2026-01/pic.jpg', + 'url' => 'https://example.com/media/2026-01/pic.jpg', + 'mime_type' => 'image/jpeg', + 'original_filename' => 'pic.jpg', + ]], + ]); + + $this->mock(MediaOptimizer::class) + ->shouldReceive('optimizeImage') + ->andReturnUsing(fn () => tap(tempnam(sys_get_temp_dir(), 'discord_test_'), fn ($f) => file_put_contents($f, str_repeat('x', 1024)))); + + Http::fake([ + config('trypost.platforms.discord.api').'/guilds/*/channels' => Http::response([['id' => '444555666', 'name' => 'general', 'type' => 0]], 200), + 'example.com/*' => Http::response(str_repeat('x', 1024), 200), + config('trypost.platforms.discord.api').'/channels/*/messages' => Http::response(['id' => '901'], 200), + ]); + + $result = $this->publisher->publish(($this->makePostPlatform)()); + + expect($result['id'])->toBe('901'); + + Http::assertSent(function ($request) { + if (! str_contains($request->url(), '/messages')) { + return false; + } + $names = collect($request->data())->pluck('name'); + + return $names->contains('payload_json') && $names->contains('files[0]'); + }); +}); + test('throws when no channel is selected', function () { expect(fn () => $this->publisher->publish(($this->makePostPlatform)(meta: []))) ->toThrow(DiscordPublishException::class);