diff --git a/components/notification/NotificationCard.vue b/components/notification/NotificationCard.vue index 052aab16..ed48ed4d 100644 --- a/components/notification/NotificationCard.vue +++ b/components/notification/NotificationCard.vue @@ -47,28 +47,8 @@ const { notification } = defineProps<{ <!-- TODO: accept request --> <AccountCard :account="notification.account" /> </template> - <template v-else-if="notification.type === 'favourite'"> - <StatusCard :status="notification.status!" :faded="true"> - <template #meta> - <div flex="~" gap-1 items-center mt1> - <div i-ri:heart-fill text-xl me-1 color-red /> - <AccountInlineInfo text-primary font-bold :account="notification.account" me1 /> - </div> - </template> - </StatusCard> - </template> - <template v-else-if="notification.type === 'reblog'"> - <StatusCard :status="notification.status!" :faded="true"> - <template #meta> - <div flex="~" gap-1 items-center mt1> - <div i-ri:repeat-fill text-xl me-1 color-green /> - <AccountInlineInfo text-primary font-bold :account="notification.account" me1 /> - </div> - </template> - </StatusCard> - </template> <template v-else-if="notification.type === 'update'"> - <StatusCard :status="notification.status!" :faded="true"> + <StatusCard :status="notification.status!" :in-notification="true" :actions="false"> <template #meta> <div flex="~" gap-1 items-center mt1> <div i-ri:edit-2-fill text-xl me-1 text-secondary /> @@ -84,6 +64,7 @@ const { notification } = defineProps<{ <StatusCard :status="notification.status!" /> </template> <template v-else> + <!-- type 'favourite' and 'reblog' should always rendered by NotificationGroupedLikes --> <div text-red font-bold> [DEV] {{ $t('notification.missing_type') }} '{{ notification.type }}' </div> diff --git a/components/notification/NotificationGroupedLikes.vue b/components/notification/NotificationGroupedLikes.vue index f87c4e08..ee81c011 100644 --- a/components/notification/NotificationGroupedLikes.vue +++ b/components/notification/NotificationGroupedLikes.vue @@ -4,21 +4,58 @@ import type { GroupedLikeNotifications } from '~/types' const { group } = defineProps<{ group: GroupedLikeNotifications }>() + +const reblogs = $computed(() => group.likes.filter(i => i.reblog)) +const likes = $computed(() => group.likes.filter(i => i.favourite && !i.reblog)) </script> <template> <article flex flex-col relative> - <StatusCard :status="group.status!" :faded="true"> - <template #meta> - <div flex flex-col gap-1 mt-1> - <div v-for="like of group.likes" :key="like.account.id" flex> - <div v-if="like.reblog" i-ri:repeat-fill text-xl me-2 color-green /> - <div v-if="like.favourite && !like.reblog" i-ri:heart-fill text-xl me-2 color-red /> - <AccountInlineInfo text-primary font-bold :account="like.account" me2 /> - <div v-if="like.favourite && like.reblog" i-ri:heart-fill text-xl me-2 color-red /> + <StatusLink :status="group.status!" pb2 pt3> + <div flex flex-col gap-2> + <div v-if="reblogs.length" flex="~ gap-1"> + <div i-ri:repeat-fill text-xl me-1 color-green /> + <template v-for="i, idx of reblogs" :key="idx"> + <AccountHoverWrapper :account="i.account"> + <NuxtLink :to="getAccountRoute(i.account)"> + <AccountAvatar text-primary font-bold :account="i.account" class="h-1.5em w-1.5em" /> + </NuxtLink> + </AccountHoverWrapper> + </template> + <div ml1> + {{ $t('notification.reblogged_post') }} </div> </div> - </template> - </StatusCard> + <div v-if="likes.length" flex="~ gap-1"> + <div i-ri:heart-fill text-xl me-1 color-red /> + <template v-for="i, idx of likes" :key="idx"> + <AccountHoverWrapper :account="i.account"> + <NuxtLink :to="getAccountRoute(i.account)"> + <AccountAvatar text-primary font-bold :account="i.account" class="h-1.5em w-1.5em" /> + </NuxtLink> + </AccountHoverWrapper> + </template> + <div ml1> + {{ $t('notification.favourited_post') }} + </div> + </div> + </div> + <div pl8 mt-1> + <StatusBody :status="group.status!" text-secondary /> + <!-- When no text content is presented, we show media instead --> + <template v-if="!group.status!.content"> + <StatusMedia + v-if="group.status!.mediaAttachments?.length" + :status="group.status!" + :is-preview="false" + pointer-events-none + /> + <StatusPoll + v-else-if="group.status!.poll" + :status="group.status!" + /> + </template> + </div> + </StatusLink> </article> </template> diff --git a/components/notification/NotificationPaginator.vue b/components/notification/NotificationPaginator.vue index ca52faf1..83a7f80e 100644 --- a/components/notification/NotificationPaginator.vue +++ b/components/notification/NotificationPaginator.vue @@ -75,9 +75,7 @@ function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] { } return } - - const { status } = group[0] - if (status && group.length > 1 && (group[0].type === 'reblog' || group[0].type === 'favourite')) { + else if (group.length && group[0].status && (group[0].type === 'reblog' || group[0].type === 'favourite')) { // All notifications in these group are reblogs or favourites of the same status const likes: GroupedAccountLike[] = [] for (const notification of group) { @@ -96,7 +94,7 @@ function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] { results.push({ id: `grouped-${id++}`, type: 'grouped-reblogs-and-favourites', - status, + status: group[0].status, likes, }) return diff --git a/components/status/StatusCard.vue b/components/status/StatusCard.vue index 1dc0a380..424ef6be 100644 --- a/components/status/StatusCard.vue +++ b/components/status/StatusCard.vue @@ -7,7 +7,7 @@ const props = withDefaults( actions?: boolean context?: mastodon.v2.FilterContext hover?: boolean - faded?: boolean + inNotification?: boolean isPreview?: boolean // If we know the prev and next status in the timeline, we can simplify the card @@ -77,19 +77,7 @@ const showReplyTo = $computed(() => !replyToMain && !directReply) </script> <template> - <div - :id="`status-${status.id}`" - ref="el" - relative flex="~ col gap1" - p="b-2 is-3 ie-4" - :class="{ 'hover:bg-active': hover }" - tabindex="0" - focus:outline-none focus-visible:ring="2 primary" - aria-roledescription="status-card" - :lang="status.language ?? undefined" - @click="onclick" - @keydown.enter="onclick" - > + <StatusLink :status="status" :hover="hover"> <!-- Upper border --> <div :h="showUpperBorder ? '1px' : '0'" w-auto bg-border mb-1 /> @@ -101,7 +89,7 @@ const showReplyTo = $computed(() => !replyToMain && !directReply) m="is-5" p="t-1 is-5" :status="status" :is-self-reply="isSelfReply" - :class="faded ? 'text-secondary-light' : ''" + :class="inNotification ? 'text-secondary-light' : ''" /> <div flex="~ col gap-1" items-center pos="absolute top-0 inset-is-0" w="77px" z--1> <template v-if="showReplyTo"> @@ -134,7 +122,7 @@ const showReplyTo = $computed(() => !replyToMain && !directReply) </div> </slot> - <div flex gap-3 :class="{ 'text-secondary': faded }"> + <div flex gap-3 :class="{ 'text-secondary': inNotification }"> <!-- Avatar --> <div relative> <div v-if="collapseRebloggedBy" absolute flex items-center justify-center top--6px px-2px py-3px rounded-full bg-base> @@ -179,9 +167,16 @@ const showReplyTo = $computed(() => !replyToMain && !directReply) </div> <!-- Content --> - <StatusContent :status="status" :newer="newer" :context="context" :is-preview="isPreview" mb2 :class="{ 'mt-2 mb1': isDM }" /> + <StatusContent + :status="status" + :newer="newer" + :context="context" + :is-preview="isPreview" + :in-notification="inNotification" + mb2 :class="{ 'mt-2 mb1': isDM }" + /> <StatusActions v-if="actions !== false" v-show="!userSettings.zenMode" :status="status" /> </div> </div> - </div> + </StatusLink> </template> diff --git a/components/status/StatusContent.vue b/components/status/StatusContent.vue index 7dc937c6..d6efd353 100644 --- a/components/status/StatusContent.vue +++ b/components/status/StatusContent.vue @@ -6,6 +6,7 @@ const { status, context } = defineProps<{ newer?: mastodon.v1.Status context?: mastodon.v2.FilterContext | 'details' isPreview?: boolean + inNotification?: boolean }>() const isDM = $computed(() => status.visibility === 'direct') diff --git a/components/status/StatusLink.vue b/components/status/StatusLink.vue new file mode 100644 index 00000000..613042e5 --- /dev/null +++ b/components/status/StatusLink.vue @@ -0,0 +1,48 @@ +<script setup lang="ts"> +import type { mastodon } from 'masto' + +const props = defineProps<{ + status: mastodon.v1.Status + hover?: boolean +}>() + +const el = ref<HTMLElement>() +const router = useRouter() +const statusRoute = $computed(() => getStatusRoute(props.status)) + +function onclick(evt: MouseEvent | KeyboardEvent) { + const path = evt.composedPath() as HTMLElement[] + const el = path.find(el => ['A', 'BUTTON', 'IMG', 'VIDEO'].includes(el.tagName?.toUpperCase())) + const text = window.getSelection()?.toString() + if (!el && !text) + go(evt) +} + +function go(evt: MouseEvent | KeyboardEvent) { + if (evt.metaKey || evt.ctrlKey) { + window.open(statusRoute.href) + } + else { + cacheStatus(props.status) + router.push(statusRoute) + } +} +</script> + +<template> + <div + :id="`status-${status.id}`" + ref="el" + relative flex="~ col gap1" + p="b-2 is-3 ie-4" + :class="{ 'hover:bg-active': hover }" + tabindex="0" + focus:outline-none focus-visible:ring="2 primary" + aria-roledescription="status-card" + :lang="status.language ?? undefined" + @click="onclick" + @keydown.enter="onclick" + > + <slot /> + </div> +</template>