Skip to content
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

feat: view-transition-api #2211

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
deeb560
proof-of-concept view-transition-api usage
horvbalint Jul 8, 2023
2115df2
built-in nuxt viewTransition + display-name animation fix
horvbalint Jul 9, 2023
6c14000
Merge branch 'main' into feat--view-transition-api
horvbalint Jan 15, 2024
f735d21
wip
horvbalint Jan 16, 2024
3be1727
works quite good
horvbalint Jan 16, 2024
83e1e1a
Merge pull request #1 from horvbalint/temp
horvbalint Jan 16, 2024
bf55726
exclude accout-hover-card from viewTransitions
horvbalint Jan 16, 2024
3061cf2
lint fix
horvbalint Jan 16, 2024
0b81088
fix statuses clipping trough the header and footer duting transition
horvbalint Jan 19, 2024
2801fc1
Merge branch 'main' into feat--view-transition-api
horvbalint Jan 19, 2024
a6b4462
Merge branch 'main' into feat--view-transition-api
horvbalint Feb 25, 2024
d9c2eac
proper enabled/disabled state
horvbalint Mar 2, 2024
a2cd18a
added preference (experimental section) + like notification account s…
horvbalint Mar 2, 2024
0a723ce
Merge branch 'main' into feat--view-transition-api
horvbalint Mar 2, 2024
e29e2bd
preference ractivity fix
horvbalint Mar 2, 2024
95e8ef9
Merge branch 'feat--view-transition-api' of https://github.com/horvba…
horvbalint Mar 2, 2024
0f1d6f1
some review fixes
horvbalint Mar 2, 2024
2187621
Merge branch 'main' into feat--view-transition-api
horvbalint Mar 17, 2024
515da7b
wait for transition end before rendering status context + review fix …
horvbalint Mar 17, 2024
b4cd163
remove unused import
horvbalint Mar 17, 2024
6169787
some more fixes + setting disabled when view-transitions are not supp…
horvbalint Mar 19, 2024
8069fc9
use transition.finished.finally instead of .then
horvbalint Mar 21, 2024
2a29728
fix typo
horvbalint Mar 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions components/account/AccountAvatar.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<script setup lang="ts">
import type { mastodon } from 'masto'

defineProps<{
const { account } = defineProps<{
account: mastodon.v1.Account
square?: boolean
}>()

const loaded = ref(false)
const error = ref(false)

const viewTransitionStyle = getViewTransitionStyles('account-avatar', { account })
</script>

<template>
<img
:key="account.avatar"
v-bind="$attrs"
width="400"
height="400"
select-none
Expand All @@ -21,8 +24,7 @@ const error = ref(false)
loading="lazy"
class="account-avatar"
:class="(loaded ? 'bg-base' : 'bg-gray:10') + (square ? ' ' : ' rounded-full')"
:style="{ 'clip-path': square ? `url(#avatar-mask)` : 'none' }"
v-bind="$attrs"
:style="{ 'clip-path': square ? `url(#avatar-mask)` : 'none', ...viewTransitionStyle }"
@load="loaded = true"
@error="error = true"
>
Expand Down
3 changes: 3 additions & 0 deletions components/account/AccountDisplayName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ const { account, hideEmojis = false } = defineProps<{
account: mastodon.v1.Account
hideEmojis?: boolean
}>()

const viewTransitionStype = getViewTransitionStyles('account-display-name', { account })
</script>

<template>
<ContentRich
:style="viewTransitionStype"
horvbalint marked this conversation as resolved.
Show resolved Hide resolved
:content="getDisplayName(account, { rich: true })"
:emojis="account.emojis"
:hide-emojis="hideEmojis"
Expand Down
3 changes: 2 additions & 1 deletion components/account/AccountHandle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const { account } = defineProps<{
}>()

const serverName = computed(() => getServerName(account))
const viewTransitionStyle = getViewTransitionStyles('account-handle', { account })
</script>

<template>
<p line-clamp-1 whitespace-pre-wrap break-all text-secondary-light leading-tight dir="ltr">
<p :style="viewTransitionStyle" line-clamp-1 whitespace-pre-wrap break-all text-secondary-light leading-tight dir="ltr">
<!-- fix: #274 only line-clamp-1 can be used here, using text-ellipsis is not valid -->
<span text-secondary>{{ getShortHandle(account) }}</span>
<span v-if="serverName" text-secondary-light>@{{ serverName }}</span>
Expand Down
4 changes: 3 additions & 1 deletion components/account/AccountHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

const { t } = useI18n()

const createdAt = useFormattedDateTime(() => account.createdAt, {
provide(viewTransitionAccountInjectionKey, account)

const createdAt = useFormattedDateTime(() => account.creaedAt, { // TODO: typo

Check failure on line 15 in components/account/AccountHeader.vue

View workflow job for this annotation

GitHub Actions / ci

Property 'creaedAt' does not exist on type 'Account$1'. Did you mean 'createdAt'?
horvbalint marked this conversation as resolved.
Show resolved Hide resolved
month: 'long',
day: 'numeric',
year: 'numeric',
Expand Down
2 changes: 2 additions & 0 deletions components/account/AccountHoverCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { account } = defineProps<{
}>()

const relationship = useRelationship(account)

provide(viewTransitionEnabledInjectionKey, false)
</script>

<template>
Expand Down
15 changes: 13 additions & 2 deletions components/account/AccountInlineInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ const { link = true, avatar = true } = defineProps<{
}>()

const userSettings = useUserSettings()

const router = useRouter()
const status = inject(viewTransitionStatusInjectionKey)
function goToAccount(account: mastodon.v1.Account) {
if (!link)
return

setViewTransitionTarget({ account, status })
router.push(getAccountRoute(account))
}
</script>

<script lang="ts">
Expand All @@ -19,10 +29,11 @@ export default {
<template>
<AccountHoverWrapper :account="account">
<NuxtLink
:to="link ? getAccountRoute(account) : undefined"
:class="link ? 'text-link-rounded -ml-1.5rem pl-1.5rem rtl-(ml0 pl-0.5rem -mr-1.5rem pr-1.5rem)' : ''"
v-bind="$attrs"
min-w-0 flex gap-2 items-center
min-w-0
flex gap-2 items-center
@click="goToAccount(account)"
>
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 />
<AccountDisplayName :account="account" :hide-emojis="getPreferences(userSettings, 'hideUsernameEmojis')" line-clamp-1 ws-pre-wrap break-all />
Expand Down
3 changes: 3 additions & 0 deletions components/main/MainContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const containerClass = computed(() => {

return 'lg:sticky lg:top-0'
})

provide(viewTransitionEnabledInjectionKey, true)
</script>

<template>
Expand All @@ -30,6 +32,7 @@ const containerClass = computed(() => {
sticky top-0 z10
pt="[env(safe-area-inset-top,0)]"
bg="[rgba(var(--rgb-bg-base),0.7)]"
style="view-transition-name: header"
class="native:lg:w-[calc(100vw-5rem)] native:xl:w-[calc(135%+(100vw-1200px)/2)]"
:class="{
'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'),
Expand Down
11 changes: 9 additions & 2 deletions components/notification/NotificationGroupedLikes.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
import type { GroupedLikeNotifications } from '~/types'

const { group } = defineProps<{
Expand All @@ -8,6 +9,12 @@ const useStarFavoriteIcon = usePreferences('useStarFavoriteIcon')

const reblogs = computed(() => group.likes.filter(i => i.reblog))
const likes = computed(() => group.likes.filter(i => i.favourite && !i.reblog))

const router = useRouter()
function goToAccount(account: mastodon.v1.Account) {
setViewTransitionTarget({ account })
router.push(getAccountRoute(account))
}
</script>

<template>
Expand All @@ -18,7 +25,7 @@ const likes = computed(() => group.likes.filter(i => i.favourite && !i.reblog))
<div i-ri:repeat-fill text-xl me-2 color-green />
<template v-for="i, idx of reblogs" :key="idx">
<AccountHoverWrapper :account="i.account">
<NuxtLink :to="getAccountRoute(i.account)">
<NuxtLink @click="goToAccount(i.account)">
<AccountAvatar text-primary font-bold :account="i.account" class="h-1.5em w-1.5em" />
</NuxtLink>
</AccountHoverWrapper>
Expand All @@ -31,7 +38,7 @@ const likes = computed(() => group.likes.filter(i => i.favourite && !i.reblog))
<div :class="useStarFavoriteIcon ? 'i-ri:star-line color-yellow' : 'i-ri:heart-line color-red'" text-xl me-2 />
<template v-for="i, idx of likes" :key="idx">
<AccountHoverWrapper :account="i.account">
<NuxtLink :to="getAccountRoute(i.account)">
<NuxtLink @click="goToAccount(i.account)">
<AccountAvatar text-primary font-bold :account="i.account" class="h-1.5em w-1.5em" />
</NuxtLink>
</AccountHoverWrapper>
Expand Down
23 changes: 16 additions & 7 deletions components/status/StatusAccountDetails.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
<script setup lang="ts">
import type { mastodon } from 'masto'

const { account, link = true } = defineProps<{
account: mastodon.v1.Account
const { status, link = true } = defineProps<{
status: mastodon.v1.Status
link?: boolean
}>()

const userSettings = useUserSettings()

const router = useRouter()
function goToAccount(account: mastodon.v1.Account) {
if (!link)
return

setViewTransitionTarget({ account, status })
router.push(getAccountRoute(account))
}
</script>

<template>
<NuxtLink
:to="link ? getAccountRoute(account) : undefined"
flex="~ col" min-w-0 md:flex="~ row gap-2" md:items-center
text-link-rounded
flex="~ col"
min-w-0 items-start md:flex="~ row gap-2" md:items-center text-link-rounded
@click="goToAccount(status.account)"
>
<AccountDisplayName :account="account" :hide-emojis="getPreferences(userSettings, 'hideUsernameEmojis')" font-bold line-clamp-1 ws-pre-wrap break-all />
<AccountHandle :account="account" class="zen-none" />
<AccountDisplayName :account="status.account" :hide-emojis="getPreferences(userSettings, 'hideUsernameEmojis')" font-bold line-clamp-1 ws-pre-wrap break-all />
<AccountHandle :account="status.account" class="zen-none" />
</NuxtLink>
</template>
4 changes: 3 additions & 1 deletion components/status/StatusActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ function reply() {
else
navigateToStatus({ status: status.value, focusReply: true })
}

const viewTransitionStyle = getViewTransitionStyles('status-actions')
</script>

<template>
<div flex justify-between items-center class="status-actions">
<div flex justify-between items-center class="status-actions" :style="viewTransitionStyle">
<div flex-1>
<StatusActionButton
:content="$t('action.reply')"
Expand Down
13 changes: 10 additions & 3 deletions components/status/StatusCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const status = computed(() => {
return props.status
})

provide(viewTransitionStatusInjectionKey, status.value)

// Use original status, avoid connecting a reblog
const directReply = computed(() => props.hasNewer || (!!status.value.inReplyToId && (status.value.inReplyToId === props.newer?.id || status.value.inReplyToId === props.newer?.reblog?.id)))
// Use reblogged status, connect it to further replies
Expand Down Expand Up @@ -67,6 +69,11 @@ const showUpperBorder = computed(() => props.newer && !directReply.value)
const showReplyTo = computed(() => !replyToMain.value && !directReply.value)

const forceShow = ref(false)

function goToAccount(account: mastodon.v1.Account) {
setViewTransitionTarget({ account, status: status.value })
router.push(getAccountRoute(account))
}
</script>

<template>
Expand Down Expand Up @@ -105,7 +112,7 @@ const forceShow = ref(false)
<div i-ri:repeat-fill me-46px text-green w-16px h-16px class="status-boosted" />
<div absolute top-1 ms-24px w-32px h-32px rounded-full>
<AccountHoverWrapper :account="rebloggedBy">
<NuxtLink :to="getAccountRoute(rebloggedBy)">
<NuxtLink @click="goToAccount(rebloggedBy)">
<AccountAvatar :account="rebloggedBy" />
</NuxtLink>
</AccountHoverWrapper>
Expand Down Expand Up @@ -136,7 +143,7 @@ const forceShow = ref(false)
<div i-ri:repeat-fill text-green w-16px h-16px />
</div>
<AccountHoverWrapper :account="status.account">
<NuxtLink :to="getAccountRoute(status.account)" rounded-full>
<NuxtLink rounded-full @click="goToAccount(status.account)">
<AccountBigAvatar :account="status.account" />
</NuxtLink>
</AccountHoverWrapper>
Expand All @@ -151,7 +158,7 @@ const forceShow = ref(false)
<!-- Account Info -->
<div flex items-center space-x-1>
<AccountHoverWrapper :account="status.account">
<StatusAccountDetails :account="status.account" />
<StatusAccountDetails :status="status" />
</AccountHoverWrapper>
<div flex-auto />
<div v-show="!getPreferences(userSettings, 'zenMode')" text-sm text-secondary flex="~ row nowrap" hover:underline whitespace-nowrap>
Expand Down
3 changes: 3 additions & 0 deletions components/status/StatusContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const hideAllMedia = computed(
return currentUser.value ? (getHideMediaByDefault(currentUser.value.account) && (!!status.mediaAttachments.length || !!status.card?.html)) : false
},
)
const viewTransitionStyle = getViewTransitionStyles('status-content')

const embeddedMediaPreference = usePreferences('experimentalEmbeddedMedia')
const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPreference.value)
</script>
Expand All @@ -40,6 +42,7 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
'pt2 pb0.5 px3.5 bg-dm rounded-4 me--1': isDM,
'ms--3.5 mt--1 ms--1': isDM && context !== 'details',
}"
:style="viewTransitionStyle"
>
<StatusBody v-if="(!isFiltered && isSensitiveNonSpoiler) || hideAllMedia" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
<StatusSpoiler :enabled="hasSpoilerOrSensitiveMedia || isFiltered" :filter="isFiltered" :sensitive-non-spoiler="isSensitiveNonSpoiler || hideAllMedia" :is-d-m="isDM">
Expand Down
2 changes: 2 additions & 0 deletions components/status/StatusDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const status = computed(() => {
return props.status
})

provide(viewTransitionStatusInjectionKey, status.value)

const createdAt = useFormattedDateTime(status.value.createdAt)

const { t } = useI18n()
Expand Down
1 change: 1 addition & 0 deletions components/status/StatusLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function go(evt: MouseEvent | KeyboardEvent) {
window.open(statusRoute.value.href)
}
else {
setViewTransitionTarget({ status: props.status })
cacheStatus(props.status)
router.push(statusRoute.value)
}
Expand Down
2 changes: 2 additions & 0 deletions composables/settings/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface PreferencesSettings {
experimentalGitHubCards: boolean
experimentalUserPicker: boolean
experimentalEmbeddedMedia: boolean
experimentalViewTransitions: boolean
}

export interface UserSettings {
Expand Down Expand Up @@ -84,6 +85,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
experimentalGitHubCards: true,
experimentalUserPicker: true,
experimentalEmbeddedMedia: false,
experimentalViewTransitions: false,
}

export function getDefaultUserSettings(locales: string[]): UserSettings {
Expand Down
81 changes: 81 additions & 0 deletions composables/view-transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { mastodon } from 'masto'

interface ViewTransitionSources {
status?: mastodon.v1.Status
account?: mastodon.v1.Account
}

interface ViewTransitionState {
targets: ViewTransitionSources
setOnPath: string
}

function getViewTransitionState() {
return useState<null | ViewTransitionState>('viewTransitionTargets', () => null)
}

export const viewTransitionEnabledInjectionKey: InjectionKey<boolean> = Symbol('whether view-transition is enabled for this component')
export const viewTransitionStatusInjectionKey: InjectionKey<undefined | mastodon.v1.Status> = Symbol('the status in context')
export const viewTransitionAccountInjectionKey: InjectionKey<undefined | mastodon.v1.Account> = Symbol('the account in context')

export function setViewTransitionTarget(targets: ViewTransitionSources) {
getViewTransitionState().value = {
targets: calcTransitionSources(targets),
setOnPath: useRoute().path,
}
}
interface ViewTransitionSources {
status?: mastodon.v1.Status
account?: mastodon.v1.Account
}

export function getViewTransitionStyles(viewTransitionName: string, sources?: ViewTransitionSources) {
const isEnabledInContext = inject(viewTransitionEnabledInjectionKey, false)
if (!isEnabledInContext)
return {}

const calcedSources = calcTransitionSources({
status: sources?.status || inject(viewTransitionStatusInjectionKey, undefined),
account: sources?.account || inject(viewTransitionAccountInjectionKey, undefined),
})

return computed(() => {
const isEnabledInPreferences = usePreferences('experimentalViewTransitions').value
if (!isEnabledInPreferences || !shouldTakePartInTransition(calcedSources))
return {}

return { 'view-transition-name': viewTransitionName }
})
}

function shouldTakePartInTransition(sources: ViewTransitionSources) {
const state = getViewTransitionState().value
if (!state?.targets.account && !state?.targets.status)
return false

const route = useRoute()
const currPath = route.path
const onProfilePage = route.name === 'account-index'
const arrivingOnProfile = state?.setOnPath !== currPath && onProfilePage

if (arrivingOnProfile) {
if (sources.account && !sources.status) // the navigation source was an avatar not a status
return state.targets.account?.id === sources.account.id
}
else {
if (state.targets.account && state.targets.account.id !== sources.account?.id)
return false

if (state.targets.status && state.targets.status.id !== sources.status?.id)
return false

return true
}
}

function calcTransitionSources(sources: ViewTransitionSources): ViewTransitionSources {
return {
status: sources.status,
account: sources.account || sources.status?.account,
}
}
Loading
Loading