-
Notifications
You must be signed in to change notification settings - Fork 0
ユーザーアイコンのキャッシュ管理の責務を移動 #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
7b99184
81896aa
2109bf9
e1fa148
85547f4
a4a4cd4
34ee59a
6382e5a
417dabb
ff4b98f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,86 +1,58 @@ | ||||||||
| <script setup lang="ts"> | ||||||||
| import { computed, ref, useAttrs } from 'vue' | ||||||||
| import { useQuery } from '@tanstack/vue-query' | ||||||||
| import { qk } from '@/api/queries/keys' | ||||||||
| 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 | ||||||||
|
||||||||
| } | ||||||||
| </script> | ||||||||
|
|
||||||||
| <template> | ||||||||
| <template v-if="showSkeleton"> | ||||||||
| <v-avatar :size="size"> | ||||||||
| <v-skeleton-loader type="image" class="w-100 h-100" /> | ||||||||
| <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 | ||||||||
|
||||||||
| v-show="!showSkeleton" | ||||||||
| class="w-100 h-100" | ||||||||
| tabindex="0" | ||||||||
| :src="iconUrl" | ||||||||
|
||||||||
| :src="iconUrl" | |
| :src="iconUrl" | |
| loading="lazy" |
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -80,6 +80,22 @@ export default defineConfig(({ mode }) => { | |||||
| disableDevLogs: true, // workbox のログを無効化 | ||||||
| globPatterns: ['**/*.{js,css,html,svg,png,jpg,webp,woff2}'], | ||||||
| navigateFallbackDenylist: [/^\/api/, /^\/login/], | ||||||
| runtimeCaching: [ | ||||||
| { | ||||||
| urlPattern: /^https:\/\/q\.trap\.jp\/api\/v3\/public\/icon\/.*/, | ||||||
| handler: 'CacheFirst', | ||||||
| options: { | ||||||
| cacheName: 'user-icons', | ||||||
| expiration: { | ||||||
| maxEntries: 200, | ||||||
| maxAgeSeconds: 7 * 24 * 60 * 60, // 1 週間 | ||||||
| }, | ||||||
| cacheableResponse: { | ||||||
| statuses: [0, 200], | ||||||
|
||||||
| statuses: [0, 200], | |
| statuses: [200], |
There was a problem hiding this comment.
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.