From 4b72aacf799d18ac23d47154de567c2e458fde6c Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Mon, 17 Aug 2026 03:42:44 +0200 Subject: [PATCH] feat(feather): complete SkyUI interaction polish --- .../components/feather/FeatherPostCard.vue | 146 +++++++++++++++--- frontend/src/stores/phone.ts | 1 + .../views/apps/FeatherApp.contract.test.ts | 91 +++++++++++ frontend/src/views/apps/FeatherApp.vue | 108 ++++++++++--- sky_phone/config/locales/en.lua | 2 +- 5 files changed, 303 insertions(+), 45 deletions(-) create mode 100644 frontend/src/views/apps/FeatherApp.contract.test.ts diff --git a/frontend/src/components/feather/FeatherPostCard.vue b/frontend/src/components/feather/FeatherPostCard.vue index 5e27fe9..934acc2 100644 --- a/frontend/src/components/feather/FeatherPostCard.vue +++ b/frontend/src/components/feather/FeatherPostCard.vue @@ -8,18 +8,14 @@ import { Share2, UserRound, } from 'lucide-vue-next' -import { - SkyButton as kButton, - SkyGlass as kGlass, - SkyIcon as kIcon, -} from '@/ui' -import { computed, ref } from 'vue' +import { SkyButton, SkyGlass, SkyIcon } from '@/ui' +import { computed, nextTick, onBeforeUnmount, ref } from 'vue' import { usePhoneStore } from '@/stores/phone' import type { FeatherPost } from '@/types/feather' const props = defineProps<{ post: FeatherPost }>() -defineEmits<{ +const emit = defineEmits<{ follow: [post: FeatherPost] media: [post: FeatherPost, index: number] menu: [post: FeatherPost] @@ -32,6 +28,9 @@ defineEmits<{ const phone = usePhoneStore() const expanded = ref(false) +const reactionPulse = ref<'like' | 'bookmark' | null>(null) +let reactionPulseFrame: number | null = null +let reactionPulseTimer: number | null = null const shouldTruncate = computed(() => props.post.body.length > 260) const visibleBody = computed(() => shouldTruncate.value && !expanded.value @@ -57,10 +56,40 @@ function relativeTime(timestamp: number): string { month: 'short', }).format(timestamp) } + +function playReactionPulse(kind: 'like' | 'bookmark'): void { + if (reactionPulseFrame !== null) + window.cancelAnimationFrame(reactionPulseFrame) + if (reactionPulseTimer !== null) window.clearTimeout(reactionPulseTimer) + reactionPulse.value = null + void nextTick(() => { + reactionPulseFrame = window.requestAnimationFrame(() => { + reactionPulse.value = kind + reactionPulseFrame = null + reactionPulseTimer = window.setTimeout(() => { + reactionPulse.value = null + reactionPulseTimer = null + }, 520) + }) + }) +} + +function react(kind: 'like' | 'bookmark'): void { + const isActivating = + kind === 'like' ? !props.post.is_liked : !props.post.is_bookmarked + if (isActivating) playReactionPulse(kind) + emit('react', props.post, kind) +} + +onBeforeUnmount(() => { + if (reactionPulseFrame !== null) + window.cancelAnimationFrame(reactionPulseFrame) + if (reactionPulseTimer !== null) window.clearTimeout(reactionPulseTimer) +})