FIX - polish FlipTok profile interactions

Improve feed follow feedback, authentication titles, connection avatar alignment, and following-list state. Keep already-followed profiles disabled and cover the behavior with store and UI contract tests.
This commit is contained in:
Leon.Schmidt
2026-08-17 13:45:53 +02:00
parent 9fe47f8794
commit 409d22e57b
4 changed files with 89 additions and 19 deletions
+18
View File
@@ -234,6 +234,24 @@ describe('FlipTok verification updates', () => {
})
})
it('marks every account in the own following list as already followed', async () => {
const followedProfile = {
...profile,
id: 8,
is_following: false,
is_owner: false,
}
vi.mocked(nuiCall).mockResolvedValue({
success: true,
data: [followedProfile],
})
const store = useFlipTokStore()
store.profile = { ...profile }
expect(await store.loadConnections(profile.id, 'following')).toBe(true)
expect(store.connections[0]?.is_following).toBe(true)
})
it('does not show a follow state when the server rejects it', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: false })
const store = useFlipTokStore()
+5
View File
@@ -238,6 +238,11 @@ export const useFlipTokStore = defineStore('fliptok', {
profileId,
})
this.connections = response.success && response.data ? response.data : []
if (mode === 'following' && profileId === this.profile?.id) {
this.connections.forEach((profile) => {
if (!profile.is_owner) profile.is_following = true
})
}
return response.success
},
async loadActivities(): Promise<boolean> {
@@ -98,11 +98,15 @@ describe('FlipTokApp Sky UI contract', () => {
expect(source).toContain("route.query.compose === '1'")
expect(source).toContain('mediaIds,')
expect(source).toContain('class="photo-slideshow"')
expect(source).toContain('@pointerdown="beginPhotoSlideDrag(video.id, $event)"')
expect(source).toContain(
'@pointerdown="beginPhotoSlideDrag(video.id, $event)"',
)
expect(source).toContain('@pointermove="updatePhotoSlideDrag"')
expect(source).toContain('@pointerup="endPhotoSlideDrag(video, $event)"')
expect(source).not.toContain('photo-slideshow__arrow')
expect(source).toContain('window.requestAnimationFrame(() => renderPhotoSlideDrag(drag))')
expect(source).toContain(
'window.requestAnimationFrame(() => renderPhotoSlideDrag(drag))',
)
expect(source).toContain('function settlePhotoSlide(')
expect(source).toContain('class="photo-slideshow__track"')
expect(source).toContain('const animation = track.animate(')
@@ -129,7 +133,10 @@ describe('FlipTokApp Sky UI contract', () => {
it('shows a centered transient follow control', () => {
expect(source).toContain('followFeedbackIds')
expect(source).toContain('follow-dot--confirmed')
expect(source).toContain('@click="followFromFeed(video)"')
expect(source).toContain('@click.stop="followFromFeed(video)"')
expect(source).toContain(
'video.is_following || followPendingIds.has(video.id)',
)
expect(source).toMatch(
/\.video-actions \.follow-dot\s*\{[^}]*min-width: 20px !important;[^}]*min-height: 20px !important;[^}]*place-items: center;/s,
)
@@ -150,10 +157,21 @@ describe('FlipTokApp Sky UI contract', () => {
expect(source).toMatch(
/\.profile-navbar-actions :deep\(\.sky-link\)\s*\{[^}]*width: 38px;[^}]*border:/s,
)
expect(source).toContain(':disabled="profile.is_following"')
expect(source).toContain(':disabled="connectionIsFollowing(profile)"')
expect(source).toContain("connectionsMode.value === 'following'")
expect(source).toContain('@click="followConnection(profile)"')
})
it('labels authentication modes and centers connection avatar fallbacks', () => {
expect(source).toContain(
":title=\"t(authMode === 'login' ? 'login' : 'register')\"",
)
expect(source).toContain('class="connection-avatar__fallback"')
expect(source).toMatch(
/\.connections-list \.connection-avatar__fallback\s*\{[^}]*display:\s*grid;[^}]*place-items:\s*center;/s,
)
})
it('supports validated custom audio links in the composer', () => {
expect(source).toContain('function validCustomMusicUrl')
expect(source).toContain('customMusicDraftUrl')
+44 -15
View File
@@ -361,9 +361,7 @@ function beginPhotoSlideDrag(videoId: string, event: PointerEvent): void {
element.setPointerCapture(event.pointerId)
}
function renderPhotoSlideDrag(
drag: NonNullable<typeof photoSlideDrag>,
): void {
function renderPhotoSlideDrag(drag: NonNullable<typeof photoSlideDrag>): void {
drag.frame = null
if (photoSlideDrag !== drag) return
const translateX = drag.startTranslateX + (drag.currentX - drag.startX)
@@ -409,7 +407,8 @@ function endPhotoSlideDrag(video: FlipTokVideo, event: PointerEvent): void {
shouldAdvance ? drag.startIndex + direction : drag.startIndex,
),
)
if (element.hasPointerCapture(pointerId)) element.releasePointerCapture(pointerId)
if (element.hasPointerCapture(pointerId))
element.releasePointerCapture(pointerId)
element.classList.remove('photo-slideshow--dragging')
photoSlideDrag = null
settlePhotoSlide(video.id, element, nextIndex)
@@ -437,7 +436,10 @@ function settlePhotoSlide(
element.classList.add('photo-slideshow--settling')
const animation = track.animate(
[
{ transform: startTransform === 'none' ? track.style.transform : startTransform },
{
transform:
startTransform === 'none' ? track.style.transform : startTransform,
},
{ transform: animationTarget },
],
{
@@ -1401,10 +1403,18 @@ async function openConnectionProfile(profile: FlipTokProfile): Promise<void> {
}
async function followConnection(profile: FlipTokProfile): Promise<void> {
if (profile.is_owner || profile.is_following) return
if (profile.is_owner || connectionIsFollowing(profile)) return
if (!(await store.followProfile(profile))) notify(t('errors.default'))
}
function connectionIsFollowing(profile: FlipTokProfile): boolean {
return (
profile.is_following ||
(connectionsMode.value === 'following' &&
currentProfile.value?.is_owner === true)
)
}
function openActions(video: FlipTokVideo): void {
selectedVideo.value = video
actionsOpen.value = true
@@ -1767,7 +1777,7 @@ onBeforeUnmount(() => {
>
<SkyNavbar
class="fliptok-auth__navbar"
:title="t('name')"
:title="t(authMode === 'login' ? 'login' : 'register')"
:scroll-el="null"
variant="medium"
/>
@@ -2037,13 +2047,15 @@ onBeforeUnmount(() => {
!video.is_owner &&
(!video.is_following || followFeedbackIds.has(video.id))
"
type="button"
class="follow-dot"
:class="{
'follow-dot--confirmed': video.is_following,
'follow-dot--pending': followPendingIds.has(video.id),
}"
:disabled="followPendingIds.has(video.id)"
@click="followFromFeed(video)"
:aria-label="video.is_following ? t('unfollow') : t('follow')"
:disabled="video.is_following || followPendingIds.has(video.id)"
@click.stop="followFromFeed(video)"
>
<Check v-if="video.is_following" /><Plus v-else />
</button>
@@ -3277,9 +3289,9 @@ onBeforeUnmount(() => {
:src="profile.avatar_url"
alt=""
/>
<template v-else>{{
initials(profile.display_name)
}}</template>
<span v-else class="connection-avatar__fallback">
{{ initials(profile.display_name) }}
</span>
</span>
</button>
</template>
@@ -3288,11 +3300,11 @@ onBeforeUnmount(() => {
v-if="!profile.is_owner"
small
rounded
:tonal="profile.is_following"
:disabled="profile.is_following"
:tonal="connectionIsFollowing(profile)"
:disabled="connectionIsFollowing(profile)"
@click="followConnection(profile)"
>{{
profile.is_following ? t('unfollow') : t('follow')
connectionIsFollowing(profile) ? t('unfollow') : t('follow')
}}</SkyButton
>
</template>
@@ -5905,12 +5917,18 @@ onBeforeUnmount(() => {
border: 2px solid #111;
border-radius: 50% !important;
background: var(--sky-app-accent) !important;
line-height: 0;
}
.video-actions .follow-dot svg {
width: 11px !important;
height: 11px !important;
margin: 0;
stroke-width: 3;
}
.video-actions .follow-dot:disabled {
opacity: 1;
}
/* No unused action row is reserved above Discover. */
@@ -6046,10 +6064,21 @@ onBeforeUnmount(() => {
}
.connections-list .connection-avatar {
display: grid;
place-items: center;
line-height: 1;
text-align: center;
}
.connections-list .connection-avatar__fallback {
width: 100%;
height: 100%;
display: grid;
place-items: center;
line-height: 1;
transform: translateY(-0.5px);
}
.connections-list :deep(.sky-button:disabled) {
opacity: 0.72;
}