Skip to content

ユーザーアイコンのキャッシュ管理の責務を移動#195

Open
kitsne241 wants to merge 10 commits intomainfrom
feat/user-icon-sw-cache
Open

ユーザーアイコンのキャッシュ管理の責務を移動#195
kitsne241 wants to merge 10 commits intomainfrom
feat/user-icon-sw-cache

Conversation

@kitsne241
Copy link
Copy Markdown
Collaborator

@kitsne241 kitsne241 commented Jan 25, 2026

ユーザーアイコンのキャッシュ管理を TanStack Query → SW に移動
UserIcon.vue のバグ修正とリファクタリングを伴う

Summary by CodeRabbit

  • New Features
    • ユーザーアイコンのPWAキャッシュを追加しました(オフライン/再訪問時の読み込み改善)。
  • UI/UX
    • アバター表示を簡素化し、読み込み・プレースホルダー・エラー表示を統一して安定性を向上しました。ツールチップ表示の動作を整理しました。
  • Chores
    • アイコン関連の内部キーを整理し、実装を簡素化しました。

Copilot AI review requested due to automatic review settings January 25, 2026 08:31
@github-actions
Copy link
Copy Markdown

@kitsne241 kitsne241 changed the title Feat/user icon sw cache ユーザーアイコンのキャッシュ管理の責務を移動 Jan 25, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates user icon caching from TanStack Query to Service Worker (SW) cache, significantly simplifying the UserIcon.vue component while improving caching strategy. The change moves icon caching closer to the network layer, allowing the browser's native caching mechanisms to handle user icons more efficiently.

Changes:

  • Added Service Worker runtime caching configuration for user icon API endpoints with CacheFirst strategy
  • Simplified UserIcon.vue by removing TanStack Query dependency and implementing native image load state management
  • Removed unused iconKeys from query keys file

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
vite.config.ts Adds Service Worker runtime caching configuration for user icons with 1-week expiration and 200 entry limit
src/components/generic/UserIcon.vue Refactors component to use native image loading with SW cache instead of TanStack Query; simplifies template structure
src/api/queries/keys.ts Removes iconKeys object that is no longer needed after migration to SW cache

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

vite.config.ts Outdated
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 週間
},
cacheableResponse: {
statuses: [0, 200],
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cacheableResponse includes status 0, which is typically for opaque responses from cross-origin requests. However, the URL pattern matches https://q.trap.jp/api/v3/public/icon/*, which appears to be a same-origin or CORS-enabled API endpoint. If the API endpoint properly returns CORS headers and status 200 responses, including status 0 may be unnecessary and could cache failed opaque responses.

Verify whether status 0 is actually needed for this endpoint, or if only status 200 should be cached.

Suggested change
statuses: [0, 200],
statuses: [200],

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +20
const isLoading = ref(true)
const hasError = ref(false)
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loading and error states are not reset when the userId prop changes. When the id prop changes (e.g., when rendering a different user's icon), the component will continue to show the skeleton loader or error state from the previous user until the new image loads or fails. This can cause visual inconsistencies where an old skeleton or error state is displayed while the new image is loading.

Add a watch effect to reset these states when userId changes.

Copilot uses AI. Check for mistakes.
Comment on lines 46 to 54
<v-tooltip
:activator="iconRef"
:open-on-hover="idTooltip"
:open-on-click="idTooltip"
:open-delay="1000"
location="top"
>
<span class="text-white font-weight-medium">{{ tooltipText }}</span>
<span class="text-white font-weight-medium">{{ `@${userId}` }}</span>
</v-tooltip>
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v-tooltip is positioned outside the v-avatar but references it via activator. This creates a DOM structure where the tooltip's visual container (div.rounded-circle) wraps both the avatar and the tooltip element. However, with the current structure, the tooltip will always be rendered in the DOM even when idTooltip is false, which could have performance implications in lists with many UserIcon components.

Consider moving the v-tooltip inside the v-avatar or conditionally rendering it only when idTooltip is true.

Copilot uses AI. Check for mistakes.
v-show="!showSkeleton"
class="w-100 h-100"
tabindex="0"
:src="iconUrl"
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The img element no longer has the loading="lazy" attribute that was present in the original implementation. Without this attribute, all user icons will be loaded immediately when the page renders, which could impact page load performance when many user icons are displayed (e.g., in the room info view or answer lists). The Service Worker cache will help with repeated loads, but the initial page load could be slower.

Consider adding loading="lazy" back to the img element to preserve lazy loading behavior.

Suggested change
:src="iconUrl"
:src="iconUrl"
loading="lazy"

Copilot uses AI. Check for mistakes.
<div class="rounded-circle overflow-hidden">
<v-avatar ref="iconRef" :size="size" class="d-block" :style="cursorStyle">
<v-skeleton-loader v-if="showSkeleton" type="image" class="w-100 h-100" />
<img
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of v-bind="attrs" means that any attributes passed to the UserIcon component (like class, style, or data attributes) will no longer be forwarded to the appropriate element. While the current usages in the codebase mainly pass standard props, this change could break existing or future usages that rely on attribute inheritance.

If attribute forwarding is not needed, consider adding inheritAttrs: false to the script setup to make this explicit. Otherwise, consider applying v-bind="$attrs" to an appropriate element.

Copilot uses AI. Check for mistakes.
@kitsne241
Copy link
Copy Markdown
Collaborator Author

image-proxy.trap.jp にはちょっとした不具合がありそうなので traQ の API に戻します
GIF アイコンに width クエリパラメータをつけてリクエストすると 500 Internal Server Error が返るっぽい

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

ユーザーアイコンの取得をVue Query依存からクライアント側のロード状態管理へ移行し、関連するクエリキー(iconKeysqk.icons)を削除し、PWA Workboxのランタイムキャッシュにユーザーアイコン用エントリを追加しました。

Changes

Cohort / File(s) Summary
Query Keys クリーンアップ
src/api/queries/keys.ts
iconKeys定数を削除し、エクスポートされるqkオブジェクトからiconsプロパティを除去。
UserIcon コンポーネント リファクタリング
src/components/generic/UserIcon.vue
Vue Query/fetchベースのデータ取得と関連ステート(loading/error/data URL等)を削除。代わりにuserId/iconUrl/cursorStyleを算出したクライアント側のロード制御(load/errorハンドラ、スケルトン表示)へ移行し、テンプレートを単一レンダリングパスへ簡素化。idTooltipにJSDoc付きのprops宣言を導入。
PWA キャッシュ構成
vite.config.ts
WorkboxのruntimeCachingにユーザーアイコンAPI向けエントリを追加(CacheFirst、キャッシュ名user-icons、maxEntries:200、maxAge:1 week、cacheableResponse statuses [0,200])。

Sequence Diagram(s)

sequenceDiagram
  participant Client as UserIcon Component
  participant Browser as Browser (img)
  participant SW as ServiceWorker (Workbox)
  participant API as Backend API

  Client->>Browser: set <img src=iconUrl>
  Browser->>SW: fetch iconUrl (request)
  alt cached
    SW->>SW: read from 'user-icons' cache
    SW-->>Browser: return cached image
  else not cached
    SW->>API: network fetch icon
    API-->>SW: image response
    SW->>SW: put response into 'user-icons' cache
    SW-->>Browser: return network image
  end
  Browser-->>Client: emit load / error
  alt load
    Client->>Client: isLoading=false, showSkeleton=false, hasError=false
  else error
    Client->>Client: isLoading=false, hasError=true, showSkeleton=true
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルは、変更内容の主要な目的(ユーザーアイコンのキャッシュ管理責務をTanStack QueryからService Workerに移動する)を明確に要約しており、実装内容と対応している。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user-icon-sw-cache

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/components/generic/UserIcon.vue (1)

14-14: useUserStore() を computed の外で呼び出す方が一般的なパターンです

Pinia の store を computed 内で毎回呼び出すのは動作しますが、一般的なパターンでは setup のトップレベルで store を取得します。これにより可読性が向上し、store の参照が明確になります。

♻️ 修正案
 import { computed, ref } from 'vue'
 import { useUserStore } from '@/store'

 const props = defineProps<{
   id?: string
   size: number
   /** クリック時に Tooltip で ID を表示 */
   idTooltip?: boolean
 }>()

 const iconRef = ref<HTMLElement | undefined>()
+const userStore = useUserStore()

-const userId = computed(() => props.id ?? useUserStore().user.id)
+const userId = computed(() => props.id ?? userStore.user.id)

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/generic/UserIcon.vue`:
- Around line 2-29: The component currently never clears hasError and never
resets loading state when iconUrl changes; update handleLoad to also set
hasError.value = false so a successful load clears the error, and add a watcher
on iconUrl (the computed used to build the image URL) to reset isLoading.value =
true and hasError.value = false whenever iconUrl changes (so switching IDs or
retries will re-run loading). Reference: hasError, isLoading, iconUrl,
handleLoad, handleError.
🧹 Nitpick comments (2)
vite.config.ts (1)

83-98: CacheFirst だとアイコン更新が最大1週間反映されません

CacheFirst はキャッシュ命中時にネットワークへ行かないため、ユーザーがアイコンを変更しても maxAgeSeconds の間は古い画像が残ります。更新の即時反映を重視するなら StaleWhileRevalidate などで裏側更新を行う方が安全です。

♻️ 変更案
-              handler: 'CacheFirst',
+              handler: 'StaleWhileRevalidate',
src/components/generic/UserIcon.vue (1)

37-43: img がフォーカス可能なのに説明がなく、アクセシビリティ上不利です

非インタラクティブなら tabindex は外し、意味のある画像であれば alt を付けてください。

♿ 修正案
       <img
         v-show="!showSkeleton"
         class="w-100 h-100"
-        tabindex="0"
         :src="iconUrl"
+        :alt="userId ? `@${userId}` : ''"
         `@load`="handleLoad"
         `@error`="handleError"
       />

Comment on lines +2 to +29
import { computed, ref } from 'vue'
import { useUserStore } from '@/store'

const props = defineProps<{ id?: string; size: number; idTooltip?: boolean }>()
// idTooltip ... クリック時に Tooltip で ID を表示するかどうか
const userId = props.id ?? useUserStore().user.id // id が指定されていない場合は自分のアイコンを表示
const props = defineProps<{
id?: string
size: number
/** クリック時に Tooltip で ID を表示 */
idTooltip?: boolean
}>()

const attrs = useAttrs()
const iconRef = ref<HTMLElement | undefined>()

const imageStyle = {
width: `${props.size}px`,
height: `${props.size}px`,
objectFit: 'contain' as const,
borderRadius: '50%',
display: 'block',
cursor: props.idTooltip ? 'pointer' : 'default',
}

const directUrl = `https://q.trap.jp/api/v3/public/icon/${userId}`
const userId = computed(() => props.id ?? useUserStore().user.id)
const iconUrl = computed(() => `https://q.trap.jp/api/v3/public/icon/${userId.value}`)
const cursorStyle = computed(() => ({ cursor: props.idTooltip ? 'pointer' : 'default' }))

const {
data: cachedIconUrl,
isLoading,
isFetching,
isError,
} = useQuery<string, Error>({
queryKey: qk.icons.user(userId),
staleTime: 24 * 60 * 60_000, // 24h
gcTime: 24 * 60 * 60_000, // 24h
retry: 0,
// データURLで保存する
queryFn: async () => {
const res = await fetch(directUrl)
if (!res.ok) throw new Error(`アイコンを取得できませんでした: ${res.status}`)
// 画像のロード状態を管理
const isLoading = ref(true)
const hasError = ref(false)
const showSkeleton = computed(() => isLoading.value || hasError.value)

const blob = await res.blob()
const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = () => reject(new Error('アイコンの変換に失敗しました'))
reader.readAsDataURL(blob)
})
return dataUrl
},
})

const showSkeleton = computed(
() => !cachedIconUrl.value || isLoading.value || isFetching.value || isError.value,
)
const handleLoad = () => {
isLoading.value = false
}

const tooltipText = `@${userId}`
const handleError = () => {
isLoading.value = false
hasError.value = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

エラー後/ID変更後にスケルトンが固定される可能性があります

hasError を解除する経路がなく、iconUrl が変わっても isLoading/hasError がリセットされないため、一時的な失敗やID切替後にアイコンが表示されなくなります。iconUrl 変更時に状態をリセットし、成功時に hasError をクリアしてください。

🛠️ 修正案
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
@@
-const iconUrl = computed(() => `https://q.trap.jp/api/v3/public/icon/${userId.value}`)
+const iconUrl = computed(() =>
+  userId.value ? `https://q.trap.jp/api/v3/public/icon/${userId.value}` : ''
+)
@@
 const showSkeleton = computed(() => isLoading.value || hasError.value)
+
+watch(iconUrl, () => {
+  isLoading.value = true
+  hasError.value = false
+})
@@
 const handleLoad = () => {
   isLoading.value = false
+  hasError.value = false
 }
🤖 Prompt for AI Agents
In `@src/components/generic/UserIcon.vue` around lines 2 - 29, The component
currently never clears hasError and never resets loading state when iconUrl
changes; update handleLoad to also set hasError.value = false so a successful
load clears the error, and add a watcher on iconUrl (the computed used to build
the image URL) to reset isLoading.value = true and hasError.value = false
whenever iconUrl changes (so switching IDs or retries will re-run loading).
Reference: hasError, isLoading, iconUrl, handleLoad, handleError.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/generic/UserIcon.vue`:
- Around line 43-50: The <img> in UserIcon.vue is missing an alt attribute;
update the component to accept or compute an accessible alt text and bind it to
the image (e.g., add an alt prop or computed like altText) so the <img> using
iconUrl and v-show="!showSkeleton" includes :alt="altText" (or a sensible
default such as 'User avatar'); ensure decorative images can be marked with an
empty alt ("") and keep existing handlers (handleLoad, handleError) unchanged.

@kitsne241 kitsne241 requested a review from mumumu6 February 14, 2026 10:43
Copy link
Copy Markdown
Member

@mumumu6 mumumu6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

タブの遷移(infoやeventなど)から戻るとcache storageに非同期でswが取りに行っているようでローディングが挟まるのが気になります。
tanstackqueryだとメモリに展開されているのでこれがないです。

また、v-imgの最適化で画面に表示されたときにfetchするようになっていますが、スクロールするたびにloadingが見えてこれも少し気になりませんか?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants