2525 class =" w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-900 transition-transform group-hover/avatar:scale-105"
2626 @error =" handleImageError"
2727 />
28- <span
29- class =" absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded opacity-0 pointer-events-none group-hover/avatar:opacity-100 transition-opacity whitespace-nowrap z-10"
30- >
31- @{{ notification.actor.username }}
32- </span >
3328 <div
3429 class =" absolute -bottom-1 -right-1 flex items-center justify-center w-5 h-5 rounded-full ring-2 ring-white dark:ring-gray-900"
3530 :class =" getIconBackgroundClass()"
4439
4540 <div class =" flex-1 min-w-0 pr-2" >
4641 <p class =" text-sm leading-snug text-gray-900 dark:text-gray-100" >
47- <router-link
48- :to =" `/@${notification.actor.username}`"
49- @click.stop =" markAsRead"
50- class =" hover:underline group/name relative"
42+ <span
43+ ref =" nameRef"
44+ @mouseenter =" handleNameHover"
45+ @mouseleave =" handleNameLeave"
46+ class =" hover:underline cursor-pointer"
5147 :class =" !notification.read_at ? 'font-bold' : 'font-semibold'"
48+ @click.stop =" navigateToProfile"
5249 >
5350 {{ notification.actor.name }}
54- <span
55- class =" absolute bottom-full left-0 mb-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded opacity-0 pointer-events-none group-hover/name:opacity-100 transition-opacity whitespace-nowrap z-10"
56- >
57- @{{ notification.actor.username }}
58- </span >
59- </router-link >
51+ </span >
6052 <span class =" text-gray-600 dark:text-gray-400 font-normal" >
6153 {{ ' ' + getNotificationMessage() }}
6254 </span >
8981
9082 <div v-else-if =" !notification.read_at" class =" w-2 h-2 bg-blue-500 rounded-full" ></div >
9183 </div >
84+
85+ <Teleport to =" body" >
86+ <UserHoverCard
87+ v-if =" !isMobile && showHoverCard"
88+ :show =" showHoverCard"
89+ :account =" notification.actor"
90+ :account-id =" notification.actor.id"
91+ :position =" hoverCardPosition"
92+ :current-user-id =" currentUserId"
93+ @mouseenter =" handleCardEnter"
94+ @mouseleave =" handleCardLeave"
95+ />
96+ </Teleport >
9297 </div >
9398</template >
9499
95100<script setup>
101+ import { ref , onMounted , onUnmounted } from ' vue'
96102import { useRouter } from ' vue-router'
97103import {
98104 HeartIcon ,
@@ -104,11 +110,16 @@ import {
104110} from ' @heroicons/vue/24/solid'
105111import { useHashids } from ' @/composables/useHashids'
106112import { useI18n } from ' vue-i18n'
113+ import UserHoverCard from ' @/components/ProfileHoverCard.vue'
107114
108115const props = defineProps ({
109116 notification: {
110117 type: Object ,
111118 required: true
119+ },
120+ currentUserId: {
121+ type: Number ,
122+ default: null
112123 }
113124})
114125
@@ -117,6 +128,14 @@ const router = useRouter()
117128const { encodeHashid } = useHashids ()
118129const { t } = useI18n ()
119130
131+ const isMobile = ref (false )
132+
133+ const showHoverCard = ref (false )
134+ const hoverCardPosition = ref ({ top: ' 0px' , left: ' 0px' })
135+ const hoverTimeout = ref (null )
136+ const isOverCard = ref (false )
137+ const nameRef = ref (null )
138+
120139const notificationConfig = {
121140 ' video.like' : {
122141 message: t (' notifications.messageTypes.videoLike' ),
@@ -180,6 +199,85 @@ const notificationConfig = {
180199 }
181200}
182201
202+ // Check if device is mobile
203+ const checkMobile = () => {
204+ isMobile .value =
205+ ' ontouchstart' in window || navigator .maxTouchPoints > 0 || window .innerWidth < 768
206+ }
207+
208+ onMounted (() => {
209+ checkMobile ()
210+ window .addEventListener (' resize' , checkMobile)
211+ })
212+
213+ onUnmounted (() => {
214+ window .removeEventListener (' resize' , checkMobile)
215+ clearTimeout (hoverTimeout .value )
216+ })
217+
218+ const calculatePosition = () => {
219+ if (! nameRef .value ) return { top: ' 0px' , left: ' 0px' }
220+
221+ const rect = nameRef .value .getBoundingClientRect ()
222+ const cardWidth = 288
223+ const cardHeight = 220
224+ const spacing = 12
225+
226+ let left = rect .left + rect .width / 2 - cardWidth / 2
227+
228+ const margin = 16
229+ if (left < margin) {
230+ left = margin
231+ } else if (left + cardWidth > window .innerWidth - margin) {
232+ left = window .innerWidth - cardWidth - margin
233+ }
234+
235+ let top = rect .top - cardHeight - spacing
236+
237+ if (top < margin) {
238+ top = rect .bottom + spacing
239+ }
240+
241+ return {
242+ top: ` ${ top} px` ,
243+ left: ` ${ left} px`
244+ }
245+ }
246+
247+ const handleNameHover = () => {
248+ if (isMobile .value ) return
249+
250+ clearTimeout (hoverTimeout .value )
251+ hoverTimeout .value = setTimeout (() => {
252+ hoverCardPosition .value = calculatePosition ()
253+ showHoverCard .value = true
254+ }, 500 )
255+ }
256+
257+ const handleNameLeave = () => {
258+ clearTimeout (hoverTimeout .value )
259+ hoverTimeout .value = setTimeout (() => {
260+ if (! isOverCard .value ) {
261+ showHoverCard .value = false
262+ }
263+ }, 200 )
264+ }
265+
266+ const handleCardEnter = () => {
267+ clearTimeout (hoverTimeout .value )
268+ isOverCard .value = true
269+ }
270+
271+ const handleCardLeave = () => {
272+ isOverCard .value = false
273+ showHoverCard .value = false
274+ }
275+
276+ const navigateToProfile = () => {
277+ markAsRead ()
278+ router .push (` /@${ props .notification .actor .username } ` )
279+ }
280+
183281const getNotificationMessage = () => {
184282 const config = notificationConfig[props .notification .type ]
185283 return config? .message || t (' notifications.messageTypes.default' )
0 commit comments