PDFアップロード直後にファイルアイコンを表示し、プレビューをポーリングで切り替える
PDFや動画などのプレビュー可能な非画像ファイルをアップロードした直後、サーバー側でサムネイルが生成される間は ファイルアイコン を表示し、バックグラウンドのポーリングでサムネイルの準備が整ったタイミングで自動的に切り替えるようになりました。
背景
この変更は、プレビュー可能な非画像ファイル(PDF・動画)のアップロード直後に生じるUXの問題を解消するものです。サーバー側では CreateBlobImageJob が非同期にサムネイルを生成するため、アップロード完了直後にプレビューURLを参照しても実体となる画像はまだ存在しません。
以前の実装では、アップロード完了後すぐにサーバーのプレビューURLを src にセットしていました。しかしサムネイル生成中はそのURLが 86×100px のSVGプレースホルダーを返すため、エディタ上に小さなプレースホルダーアイコン画像が表示されてしまっていました。previewable? ブール値のバグ修正(bc3#10269)によってPDFが正しくプレビュー可能として扱われるようになった後、この非同期生成による見た目の問題が顕在化したことが本PRの背景です。
適切な対処としては、サムネイルが存在しない間はファイルアイコンを表示し続け、準備ができたタイミングで切り替えることです。これにより、不完全なプレースホルダー画像をユーザーに見せることなく、自然なUXを提供できます。
技術的な変更
pendingPreview フラグを起点に、アップロード完了直後の表示とポーリングによるプレビュー取得が連動する仕組みが追加されました。
action_text_attachment_upload_node.js の変更(フラグの生成):
アップロードノードからアタッチメントノードへの変換時に、pendingPreview フラグを計算して渡します。
// 変更後
pendingPreview: this.blob.previewable && !this.uploadNode.isPreviewableImage
previewable(サーバーがプレビューURLを持つ)かつ isPreviewableImage(画像そのものではない)の場合に true となります。つまりPDFや動画のみが対象で、PNG・JPEGなどの画像ファイルはこのフローに入りません。
action_text_attachment_node.js の変更(DOM生成とポーリング):
createDOM() のエントリポイントに pendingPreview の分岐が追加され、専用のプライベートメソッド #createDOMForPendingPreview() に処理が委譲されます。
createDOM() {
if (this.uploadError) return this.createDOMForError()
if (this.pendingPreview) return this.#createDOMForPendingPreview() // 追加
// ... 既存の処理
}
#createDOMForPendingPreview() {
const figure = this.createAttachmentFigure(false)
figure.appendChild(this.#createDOMForFile())
figure.appendChild(this.#createDOMForNotImage())
this.#pollForPreview(figure)
return figure
}
ポーリングの仕組み:
#pollForPreview() は初回3秒の遅延から始まる指数バックオフで最大10回プレビューURLにリクエストを送ります。レスポンスとして返ってくる画像の寸法が 150px超 であれば本物のサムネイルと判定し、ファイルアイコン表示からプレビュー画像表示へと figure の内容を差し替えます。86×100pxのSVGプレースホルダーはこの閾値を下回るため、誤検知を防げます。ノードがDOMから削除された場合や10回試行しても取得できなかった場合は、ファイルアイコン表示のまま静かにフォールバックします。
テストの更新:
ブラウザテスト(attachments.test.js)とシステムテスト(attachments_test.rb)の両方が更新され、アップロード直後に attachment--file クラスが付いていること、img 要素が存在しないこと、.attachment__icon と .attachment__name が表示されることを検証します。また active_storage_mock.js も拡張され、application/pdf と video/* のコンテンツタイプに対してモックが previewable: true と適切なプレビューURLを返すようになりました。
設計判断
画像寸法による実体判定 は、APIの追加なしにポーリング結果を判定できるシンプルな手法です。サーバーが返すSVGプレースホルダーが固定サイズ(86×100px)であることを利用し、150pxという閾値で本物のサムネイルと区別します。サーバー側のAPIを変更せずクライアントだけで完結する設計は、変更範囲を最小化しています。
pendingPreview フラグをノードのプロパティとして持つ 設計により、createDOM() の責務が明確に分岐しています。既存の uploadError フラグと同じパターンで追加されており、コードの一貫性が保たれています。pendingPreview が true のノードは、ポーリング完了後にDOMを直接書き換えることで状態を更新します。Lexicalのノード再レンダリングを経由せずDOMを直接操作している点は、非同期の外部イベント(サムネイル生成完了)をトリガーとするこのユースケースに適した選択です。
isPreviewableImage との組み合わせ で対象ファイルを絞っている点も重要です。画像ファイルはアップロード時にクライアント側でプレビューを生成できるため、このポーリング機構は不要です。PDFや動画のみをポーリング対象とすることで、不要なネットワークリクエストを回避しています。
まとめ
この変更は、非同期サムネイル生成というサーバー側の制約をクライアントが賢く扱うための実装です。プレースホルダーSVGの固定サイズという既存の特性を閾値判定に活用し、APIの変更なしにポーリングによる自動切り替えを実現している点が、シンプルかつ現実的な設計判断として参考になります。