PDFアップロード直後にファイルアイコンを表示し、プレビューをポーリングで切り替える

basecamp/lexxy

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/pdfvideo/* のコンテンツタイプに対してモックが previewable: true と適切なプレビューURLを返すようになりました。

設計判断

画像寸法による実体判定 は、APIの追加なしにポーリング結果を判定できるシンプルな手法です。サーバーが返すSVGプレースホルダーが固定サイズ(86×100px)であることを利用し、150pxという閾値で本物のサムネイルと区別します。サーバー側のAPIを変更せずクライアントだけで完結する設計は、変更範囲を最小化しています。

pendingPreview フラグをノードのプロパティとして持つ 設計により、createDOM() の責務が明確に分岐しています。既存の uploadError フラグと同じパターンで追加されており、コードの一貫性が保たれています。pendingPreviewtrue のノードは、ポーリング完了後にDOMを直接書き換えることで状態を更新します。Lexicalのノード再レンダリングを経由せずDOMを直接操作している点は、非同期の外部イベント(サムネイル生成完了)をトリガーとするこのユースケースに適した選択です。

isPreviewableImage との組み合わせ で対象ファイルを絞っている点も重要です。画像ファイルはアップロード時にクライアント側でプレビューを生成できるため、このポーリング機構は不要です。PDFや動画のみをポーリング対象とすることで、不要なネットワークリクエストを回避しています。

まとめ

この変更は、非同期サムネイル生成というサーバー側の制約をクライアントが賢く扱うための実装です。プレースホルダーSVGの固定サイズという既存の特性を閾値判定に活用し、APIの変更なしにポーリングによる自動切り替えを実現している点が、シンプルかつ現実的な設計判断として参考になります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
44128305

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の3部構成が明確に適用されており、全ての必須要素が含まれています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)およびPR番号のリンク記法([#956](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

`CreateBlobImageJob`, `Lexical`, `DOM`, `指数バックオフ`などの専門用語が説明なしに使用されており、対象読者であるエンジニアに適した技術レベルの内容となっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiff情報と完全に一致しています。ファイル名も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`pendingPreview`, `指数バックオフ`, `ポーリング`などの技術用語が、PR Descriptionの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

ポーリングの仕様(3秒遅延、10回試行)やサムネイル判定の閾値(150px)など、技術的な説明はPR Descriptionの内容と一致しており、正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのTitle, Description, Diff情報で裏付けられており、推測や憶測に基づくハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#956)、画像の寸法(86×100px, 150px)、ポーリングの回数(10回)など、記事に含まれる数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「PDFアップロード直後にファイルアイコンを表示し、プレビューをポーリングで切り替える」は、PRのTitle「Show file icon for freshly uploaded PDFs, poll for preview」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事にはPR情報に記載のない外部知識(バージョンのサポート状況、リリース日程など)は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「以前の実装では」「〜ようになりました」といった時間表現は、PRによる変更の前後関係を正確に表しており、歪曲はありません。