forked from Mirrors/elk
Merge branch 'main' into feat-increase-status-preview-card-image-quality
This commit is contained in:
commit
f73219f9bd
19 changed files with 213 additions and 90 deletions
|
@ -1 +1 @@
|
||||||
MOCK_USER='{"user":{"server":"universeodon.com","token":"BLMfvYGgiEPgLpiunVS0JYxxqzga3S58C60DDwu1jvw","account":{"id":"109424142224653388","username":"elkdev","acct":"elkdev","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":0,"followingCount":0,"statusesCount":0,"lastStatusAt":null,"noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"novae@universeodon.com","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}'
|
MOCK_USER='{"user":{"server":"universeodon.com","token":"BLMfvYGgiEPgLpiunVS0JYxxqzga3S58C60DDwu1jvw","account":{"id":"109424142224653388","username":"elkdev","acct":"elkdev@universeodon.com","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":0,"followingCount":0,"statusesCount":0,"lastStatusAt":null,"noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"novae@universeodon.com","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}'
|
||||||
|
|
5
components/account/AccountBotIndicator.vue
Normal file
5
components/account/AccountBotIndicator.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<div flex="~" items-center border="~" rounded-md px-2 text-xs>
|
||||||
|
BOT
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -76,11 +76,14 @@ watchEffect(() => {
|
||||||
<AccountAvatar :account="account" hover:opacity-90 transition-opacity />
|
<AccountAvatar :account="account" hover:opacity-90 transition-opacity />
|
||||||
</button>
|
</button>
|
||||||
<div flex flex-col>
|
<div flex flex-col>
|
||||||
|
<div flex justify-between>
|
||||||
<ContentRich
|
<ContentRich
|
||||||
font-bold sm:text-2xl text-xl
|
font-bold sm:text-2xl text-xl
|
||||||
:content="getDisplayName(account, { rich: true })"
|
:content="getDisplayName(account, { rich: true })"
|
||||||
:emojis="account.emojis"
|
:emojis="account.emojis"
|
||||||
/>
|
/>
|
||||||
|
<AccountBotIndicator v-if="account.bot" />
|
||||||
|
</div>
|
||||||
<AccountHandle :account="account" />
|
<AccountHandle :account="account" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,11 +20,14 @@ defineOptions({
|
||||||
<AccountAvatar :account="account" w-12 h-12 />
|
<AccountAvatar :account="account" w-12 h-12 />
|
||||||
</AccountHoverWrapper>
|
</AccountHoverWrapper>
|
||||||
<div flex="~ col" shrink overflow-hidden>
|
<div flex="~ col" shrink overflow-hidden>
|
||||||
|
<div flex="~" gap-2>
|
||||||
<ContentRich
|
<ContentRich
|
||||||
font-bold line-clamp-1 ws-pre-wrap break-all
|
font-bold line-clamp-1 ws-pre-wrap break-all
|
||||||
:content="getDisplayName(account, { rich: true })"
|
:content="getDisplayName(account, { rich: true })"
|
||||||
:emojis="account.emojis"
|
:emojis="account.emojis"
|
||||||
/>
|
/>
|
||||||
|
<AccountBotIndicator v-if="account.bot" />
|
||||||
|
</div>
|
||||||
<AccountHandle :account="account" text-sm text-secondary-light />
|
<AccountHandle :account="account" text-sm text-secondary-light />
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -13,7 +13,7 @@ const { link = true, avatar = true } = defineProps<{
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="link ? getAccountRoute(account) : undefined"
|
:to="link ? getAccountRoute(account) : undefined"
|
||||||
:class="link ? 'text-link-rounded ml-0 pl-0' : ''"
|
:class="link ? 'text-link-rounded ml-0 pl-0' : ''"
|
||||||
min-w-0 flex gap-1 items-center
|
min-w-0 flex gap-2 items-center
|
||||||
>
|
>
|
||||||
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 />
|
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 />
|
||||||
<ContentRich
|
<ContentRich
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div flex="~" gap-1 items-center absolute top-0 pt-2 left-0 px-3>
|
<div flex="~" gap-1 items-center absolute top-0 left-0 py-3 px-4>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -9,47 +9,55 @@ const { notification } = defineProps<{
|
||||||
<template>
|
<template>
|
||||||
<article flex flex-col relative>
|
<article flex flex-col relative>
|
||||||
<template v-if="notification.type === 'follow'">
|
<template v-if="notification.type === 'follow'">
|
||||||
<div flex ml-4 items-center absolute class="-top-2.5" right-2 px-2>
|
<div flex items-center absolute px-3 py-3 bg-base rounded-br-3 top-0 left-0>
|
||||||
<div i-ri:user-follow-fill mr-1 color-primary />
|
<div i-ri:user-follow-fill mr-1 color-primary />
|
||||||
<AccountInlineInfo :account="notification.account" mr1 />
|
<ContentRich
|
||||||
|
text-primary mr-1 font-bold line-clamp-1 ws-pre-wrap break-all
|
||||||
|
:content="getDisplayName(notification.account, { rich: true })"
|
||||||
|
:emojis="notification.account.emojis"
|
||||||
|
/>
|
||||||
<span ws-nowrap>
|
<span ws-nowrap>
|
||||||
{{ $t('notification.followed_you') }}
|
{{ $t('notification.followed_you') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<AccountCard :account="notification.account" />
|
<AccountBigCard :account="notification.account" />
|
||||||
</template>
|
</template>
|
||||||
<template v-if="notification.type === 'admin.sign_up'">
|
<template v-else-if="notification.type === 'admin.sign_up'">
|
||||||
<div flex p2 items-center gap-2>
|
<div flex p3 items-center bg-shaded>
|
||||||
<div i-ri:admin-fill mr-1 color-purple />
|
<div i-ri:admin-fill mr-1 color-purple />
|
||||||
<span>New Sign Up</span>
|
<ContentRich
|
||||||
|
text-purple mr-1 font-bold line-clamp-1 ws-pre-wrap break-all
|
||||||
|
:content="getDisplayName(notification.account, { rich: true })"
|
||||||
|
:emojis="notification.account.emojis"
|
||||||
|
/>
|
||||||
|
<span>signed up</span>
|
||||||
</div>
|
</div>
|
||||||
<AccountCard :account="notification.account" px2 pb2 />
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="notification.type === 'follow_request'">
|
<template v-else-if="notification.type === 'follow_request'">
|
||||||
<div flex ml-4 items-center class="-top-2.5" absolute right-2 px-2>
|
<div flex ml-4 items-center class="-top-2.5" absolute right-2 px-2>
|
||||||
<div i-ri:user-follow-fill mr-1 />
|
<div i-ri:user-follow-fill text-xl mr-1 />
|
||||||
<AccountInlineInfo :account="notification.account" mr1 />
|
<AccountInlineInfo :account="notification.account" mr1 />
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO: accept request -->
|
<!-- TODO: accept request -->
|
||||||
<AccountCard :account="notification.account" />
|
<AccountCard :account="notification.account" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="notification.type === 'favourite'">
|
<template v-else-if="notification.type === 'favourite'">
|
||||||
<CommonMetaWrapper>
|
<CommonMetaWrapper z-1>
|
||||||
<div i-ri:heart-fill mr-1 color-red />
|
<div i-ri:heart-fill text-xl mr-1 color-red />
|
||||||
<AccountInlineInfo :account="notification.account" mr1 />
|
<AccountInlineInfo text-primary font-bold :account="notification.account" mr1 />
|
||||||
</CommonMetaWrapper>
|
</CommonMetaWrapper>
|
||||||
<StatusCard :status="notification.status!" :decorated="true" />
|
<StatusCard op50 hover:op100 :status="notification.status!" :decorated="true" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="notification.type === 'reblog'">
|
<template v-else-if="notification.type === 'reblog'">
|
||||||
<CommonMetaWrapper>
|
<CommonMetaWrapper z-1>
|
||||||
<div i-ri:repeat-fill mr-1 color-green />
|
<div i-ri:repeat-fill text-xl mr-1 color-green />
|
||||||
<AccountInlineInfo :account="notification.account" mr1 />
|
<AccountInlineInfo text-primary font-bold :account="notification.account" mr1 />
|
||||||
</CommonMetaWrapper>
|
</CommonMetaWrapper>
|
||||||
<StatusCard :status="notification.status!" :decorated="true" />
|
<StatusCard op50 hover:op100 :status="notification.status!" :decorated="true" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="notification.type === 'update'">
|
<template v-else-if="notification.type === 'update'">
|
||||||
<CommonMetaWrapper>
|
<CommonMetaWrapper z-1>
|
||||||
<div i-ri:edit-2-fill mr-1 text-secondary />
|
<div i-ri:edit-2-fill text-xl mr-1 text-secondary />
|
||||||
<AccountInlineInfo :account="notification.account" mr1 />
|
<AccountInlineInfo :account="notification.account" mr1 />
|
||||||
<span ws-nowrap>
|
<span ws-nowrap>
|
||||||
{{ $t('notification.update_status') }}
|
{{ $t('notification.update_status') }}
|
||||||
|
|
21
components/notification/NotificationGroupedLikes.vue
Normal file
21
components/notification/NotificationGroupedLikes.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GroupedLikeNotifications } from '~/types'
|
||||||
|
|
||||||
|
const { group } = defineProps<{
|
||||||
|
group: GroupedLikeNotifications
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article flex flex-col relative>
|
||||||
|
<div flex flex-col class="-mb-12" py-3>
|
||||||
|
<div v-for="like of group.likes" :key="like.account.id" flex px-3 py-1>
|
||||||
|
<div v-if="like.reblog" i-ri:repeat-fill text-xl mr-2 color-green />
|
||||||
|
<div v-if="like.favourite && !like.reblog" i-ri:heart-fill text-xl mr-2 color-red />
|
||||||
|
<AccountInlineInfo text-primary font-bold :account="like.account" mr2 />
|
||||||
|
<div v-if="like.favourite && like.reblog" i-ri:heart-fill text-xl mr-2 color-red />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatusCard op50 hover:op100 :status="group.status!" :decorated="true" />
|
||||||
|
</article>
|
||||||
|
</template>
|
|
@ -1,45 +1,90 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Notification, Paginator, WsEvents } from 'masto'
|
import type { Notification, Paginator, WsEvents } from 'masto'
|
||||||
import type { GroupedNotifications } from '~/types'
|
import type { GroupedAccountLike, NotificationSlot } from '~/types'
|
||||||
|
|
||||||
const { paginator, stream } = defineProps<{
|
const { paginator, stream } = defineProps<{
|
||||||
paginator: Paginator<any, Notification[]>
|
paginator: Paginator<any, Notification[]>
|
||||||
stream?: WsEvents
|
stream?: WsEvents
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function groupItems(items: Notification[]): (Notification | GroupedNotifications)[] {
|
const groupCapacity = Number.MAX_VALUE // No limit
|
||||||
const results: (Notification | GroupedNotifications)[] = []
|
const minFollowGroupSize = 5 // Below this limit, show a profile card for each follow
|
||||||
|
|
||||||
|
// Group by type (and status when applicable)
|
||||||
|
const groupId = (item: Notification): string => {
|
||||||
|
// If the update is related to an status, group notifications from the same account (boost + favorite the same status)
|
||||||
|
const id = item.status
|
||||||
|
? {
|
||||||
|
status: item.status?.id,
|
||||||
|
type: (item.type === 'reblog' || item.type === 'favourite') ? 'like' : item.type,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: item.type,
|
||||||
|
}
|
||||||
|
return JSON.stringify(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupItems(items: Notification[]): NotificationSlot[] {
|
||||||
|
const results: NotificationSlot[] = []
|
||||||
|
|
||||||
let id = 0
|
let id = 0
|
||||||
let followGroup: Notification[] = []
|
let currentGroupId = ''
|
||||||
|
let currentGroup: Notification[] = []
|
||||||
|
const processGroup = () => {
|
||||||
|
if (currentGroup.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
const bump = () => {
|
const group = currentGroup
|
||||||
const alwaysGroup = true
|
currentGroup = []
|
||||||
if (!alwaysGroup && followGroup.length === 1) {
|
|
||||||
results.push(followGroup[0])
|
// Only group follow notifications when there are too many in a row
|
||||||
followGroup = []
|
// This normally happens when you transfer an account, if not, show
|
||||||
}
|
// a big profile card for each follow
|
||||||
else if (followGroup.length > 0) {
|
if (group[0].type === 'follow' && group.length > minFollowGroupSize) {
|
||||||
results.push({
|
results.push({
|
||||||
id: `grouped-${id++}`,
|
id: `grouped-${id++}`,
|
||||||
type: 'grouped-follow',
|
type: `grouped-${group[0].type}`,
|
||||||
items: followGroup,
|
items: group,
|
||||||
})
|
})
|
||||||
followGroup = []
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { status } = group[0]
|
||||||
|
if (status && group.length > 1 && (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) {
|
||||||
|
let like = likes.find(like => like.account.id === notification.account.id)
|
||||||
|
if (!like) {
|
||||||
|
like = { account: notification.account }
|
||||||
|
likes.push(like)
|
||||||
|
}
|
||||||
|
like[notification.type === 'reblog' ? 'reblog' : 'favourite'] = notification
|
||||||
|
}
|
||||||
|
likes.sort((a, b) => b.reblog && !a.reblog ? 1 : -1)
|
||||||
|
results.push({
|
||||||
|
id: `grouped-${id++}`,
|
||||||
|
type: 'grouped-reblogs-and-favourites',
|
||||||
|
status,
|
||||||
|
likes,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(...group)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type === 'follow') {
|
const itemId = groupId(item)
|
||||||
followGroup.push(item)
|
// Finalize group if it already has too many notifications
|
||||||
}
|
if (currentGroupId !== itemId || currentGroup.length >= groupCapacity)
|
||||||
else {
|
processGroup()
|
||||||
bump()
|
|
||||||
results.push(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bump()
|
currentGroup.push(item)
|
||||||
|
currentGroupId = itemId
|
||||||
|
}
|
||||||
|
// Finalize remaining groups
|
||||||
|
processGroup()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -48,7 +93,7 @@ const { clearNotifications } = useNotifications()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPaginator :paginator="paginator" :stream="stream" event-type="notification">
|
<CommonPaginator :paginator="paginator" :stream="stream" :eager="3" event-type="notification">
|
||||||
<template #updater="{ number, update }">
|
<template #updater="{ number, update }">
|
||||||
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="() => { update(); clearNotifications() }">
|
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="() => { update(); clearNotifications() }">
|
||||||
{{ $t('timeline.show_new_items', [number]) }}
|
{{ $t('timeline.show_new_items', [number]) }}
|
||||||
|
@ -61,6 +106,11 @@ const { clearNotifications } = useNotifications()
|
||||||
:items="item"
|
:items="item"
|
||||||
border="b base"
|
border="b base"
|
||||||
/>
|
/>
|
||||||
|
<NotificationGroupedLikes
|
||||||
|
v-else-if="item.type === 'grouped-reblogs-and-favourites'"
|
||||||
|
:group="item"
|
||||||
|
border="b base"
|
||||||
|
/>
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
v-else
|
v-else
|
||||||
:notification="item"
|
:notification="item"
|
||||||
|
|
|
@ -66,7 +66,7 @@ const avatarOnAvatar = $(computedEager(() => useFeatureFlags().experimentalAvata
|
||||||
<div i-ri:repeat-fill mr-1 text-primary />
|
<div i-ri:repeat-fill mr-1 text-primary />
|
||||||
<AccountInlineInfo font-bold :account="rebloggedBy" :avatar="!avatarOnAvatar" />
|
<AccountInlineInfo font-bold :account="rebloggedBy" :avatar="!avatarOnAvatar" />
|
||||||
</CommonMetaWrapper>
|
</CommonMetaWrapper>
|
||||||
<div v-if="decorated || rebloggedBy || (showReplyTo && status.inReplyToAccountId)" h-4 />
|
<div v-if="decorated || rebloggedBy || (showReplyTo && status.inReplyToAccountId)" h-6 />
|
||||||
<div flex gap-4>
|
<div flex gap-4>
|
||||||
<div relative>
|
<div relative>
|
||||||
<AccountHoverWrapper :account="status.account" :class="rebloggedBy && avatarOnAvatar ? 'mt-4' : 'mt-1'">
|
<AccountHoverWrapper :account="status.account" :class="rebloggedBy && avatarOnAvatar ? 'mt-4' : 'mt-1'">
|
||||||
|
@ -74,7 +74,7 @@ const avatarOnAvatar = $(computedEager(() => useFeatureFlags().experimentalAvata
|
||||||
<AccountAvatar w-12 h-12 :account="status.account" />
|
<AccountAvatar w-12 h-12 :account="status.account" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</AccountHoverWrapper>
|
</AccountHoverWrapper>
|
||||||
<div v-if="(rebloggedBy && avatarOnAvatar && rebloggedBy.id !== status.account.id)" absolute class="-top-1 -left-2" w-8 h-8 border-bg-base border-3 rounded-full>
|
<div v-if="(rebloggedBy && avatarOnAvatar && rebloggedBy.id !== status.account.id)" absolute class="-top-2 -left-2" w-9 h-9 border-bg-base border-3 rounded-full>
|
||||||
<AccountAvatar :account="rebloggedBy" />
|
<AccountAvatar :account="rebloggedBy" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,6 +85,7 @@ const avatarOnAvatar = $(computedEager(() => useFeatureFlags().experimentalAvata
|
||||||
</AccountHoverWrapper>
|
</AccountHoverWrapper>
|
||||||
<div flex-auto />
|
<div flex-auto />
|
||||||
<div v-if="!isZenMode" text-sm text-secondary flex="~ row nowrap" hover:underline>
|
<div v-if="!isZenMode" text-sm text-secondary flex="~ row nowrap" hover:underline>
|
||||||
|
<AccountBotIndicator v-if="status.account.bot" mr-2 />
|
||||||
<CommonTooltip :content="createdAt">
|
<CommonTooltip :content="createdAt">
|
||||||
<a :title="status.createdAt" :href="getStatusRoute(status).href" @click.prevent="go($event)">
|
<a :title="status.createdAt" :href="getStatusRoute(status).href" @click.prevent="go($event)">
|
||||||
<time text-sm ws-nowrap hover:underline :datetime="status.createdAt">
|
<time text-sm ws-nowrap hover:underline :datetime="status.createdAt">
|
||||||
|
|
|
@ -9,7 +9,7 @@ const account = useAccountById(status.inReplyToAccountId)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="status.inReplyToAccountId" absolute top-0 pt-2 right-0 px-4 flex="~ wrap" gap-1>
|
<div v-if="status.inReplyToAccountId" absolute top-0 right-0 px-4 py-3 flex="~ wrap" gap-1>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="status.inReplyToId"
|
v-if="status.inReplyToId"
|
||||||
flex="~" items-center font-bold text-sm text-secondary gap-1
|
flex="~" items-center font-bold text-sm text-secondary gap-1
|
||||||
|
|
|
@ -15,7 +15,8 @@ export function setCached(key: string, value: any, override = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatus(id: string, force = false): Promise<Status> {
|
export function fetchStatus(id: string, force = false): Promise<Status> {
|
||||||
const key = `status:${id}`
|
const server = currentServer.value
|
||||||
|
const key = `${server}:status:${id}`
|
||||||
const cached = cache.get(key)
|
const cached = cache.get(key)
|
||||||
if (cached && !force)
|
if (cached && !force)
|
||||||
return cached
|
return cached
|
||||||
|
@ -32,30 +33,37 @@ export function fetchAccountById(id?: string | null): Promise<Account | null> {
|
||||||
if (!id)
|
if (!id)
|
||||||
return Promise.resolve(null)
|
return Promise.resolve(null)
|
||||||
|
|
||||||
const key = `account:${id}`
|
const server = currentServer.value
|
||||||
|
const key = `${server}:account:${id}`
|
||||||
const cached = cache.get(key)
|
const cached = cache.get(key)
|
||||||
if (cached)
|
if (cached)
|
||||||
return cached
|
return cached
|
||||||
|
const uri = currentInstance.value?.uri
|
||||||
const promise = useMasto().accounts.fetch(id)
|
const promise = useMasto().accounts.fetch(id)
|
||||||
.then((account) => {
|
.then((r) => {
|
||||||
cacheAccount(account, true)
|
if (!r.acct.includes('@') && uri)
|
||||||
return account
|
r.acct = `${r.acct}@${uri}`
|
||||||
|
|
||||||
|
cacheAccount(r, server, true)
|
||||||
|
return r
|
||||||
})
|
})
|
||||||
cache.set(key, promise)
|
cache.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountByHandle(acct: string): Promise<Account> {
|
export async function fetchAccountByHandle(acct: string): Promise<Account> {
|
||||||
const key = `account:${acct}`
|
const server = currentServer.value
|
||||||
|
const key = `${server}:account:${acct}`
|
||||||
const cached = cache.get(key)
|
const cached = cache.get(key)
|
||||||
if (cached)
|
if (cached)
|
||||||
return cached
|
return cached
|
||||||
|
const uri = currentInstance.value?.uri
|
||||||
const account = useMasto().accounts.lookup({ acct })
|
const account = useMasto().accounts.lookup({ acct })
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.acct.includes('@') && currentInstance.value)
|
if (!r.acct.includes('@') && uri)
|
||||||
r.acct = `${r.acct}@${currentInstance.value.uri}`
|
r.acct = `${r.acct}@${uri}`
|
||||||
|
|
||||||
cacheAccount(r, true)
|
cacheAccount(r, server, true)
|
||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
cache.set(key, account)
|
cache.set(key, account)
|
||||||
|
@ -70,11 +78,11 @@ export function useAccountById(id?: string | null) {
|
||||||
return useAsyncState(() => fetchAccountById(id), null).state
|
return useAsyncState(() => fetchAccountById(id), null).state
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cacheStatus(status: Status, override?: boolean) {
|
export function cacheStatus(status: Status, server = currentServer.value, override?: boolean) {
|
||||||
setCached(`status:${status.id}`, status, override)
|
setCached(`${server}:status:${status.id}`, status, override)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cacheAccount(account: Account, override?: boolean) {
|
export function cacheAccount(account: Account, server = currentServer.value, override?: boolean) {
|
||||||
setCached(`account:${account.id}`, account, override)
|
setCached(`${server}:account:${account.id}`, account, override)
|
||||||
setCached(`account:${account.acct}`, account, override)
|
setCached(`${server}:account:${account.acct}`, account, override)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,12 +31,6 @@ export const currentInstance = computed<null | Instance>(() => currentUserId.val
|
||||||
export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
|
export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
|
||||||
|
|
||||||
export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: AccountCredentials }) {
|
export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: AccountCredentials }) {
|
||||||
if (user) {
|
|
||||||
const existing = users.value.find(u => u.server === user.server && u.token === user.token)
|
|
||||||
if (existing && currentUserId.value !== user.account?.id)
|
|
||||||
currentUserId.value = user.account?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -76,7 +70,7 @@ export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: Ac
|
||||||
|
|
||||||
setMasto(masto)
|
setMasto(masto)
|
||||||
|
|
||||||
if ('server' in route.params) {
|
if ('server' in route.params && user?.token) {
|
||||||
await router.push({
|
await router.push({
|
||||||
...route,
|
...route,
|
||||||
force: true,
|
force: true,
|
||||||
|
@ -110,7 +104,7 @@ export async function signout() {
|
||||||
currentUserId.value = users.value[0]?.account?.id
|
currentUserId.value = users.value[0]?.account?.id
|
||||||
|
|
||||||
if (!currentUserId.value)
|
if (!currentUserId.value)
|
||||||
await useRouter().push(`/${currentServer.value}/public`)
|
await useRouter().push('/')
|
||||||
|
|
||||||
await loginTo(currentUser.value)
|
await loginTo(currentUser.value)
|
||||||
}
|
}
|
||||||
|
@ -127,7 +121,7 @@ export const useNotifications = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connect(): Promise<void> {
|
async function connect(): Promise<void> {
|
||||||
if (!id || notifications[id])
|
if (!id || notifications[id] || !currentUser.value?.token)
|
||||||
return
|
return
|
||||||
|
|
||||||
const masto = useMasto()
|
const masto = useMasto()
|
||||||
|
|
|
@ -6,7 +6,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
if (!('server' in to.params))
|
if (!('server' in to.params))
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!currentUser.value) {
|
const user = currentUser.value
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
if (from.params.server !== to.params.server) {
|
if (from.params.server !== to.params.server) {
|
||||||
await loginTo({
|
await loginTo({
|
||||||
server: to.params.server as string,
|
server: to.params.server as string,
|
||||||
|
@ -16,7 +18,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to additionally resolve an id if we're already logged in
|
// No need to additionally resolve an id if we're already logged in
|
||||||
if (currentUser.value.server === to.params.server)
|
if (user.server === to.params.server)
|
||||||
return
|
return
|
||||||
|
|
||||||
// Tags don't need to be redirected to a local id
|
// Tags don't need to be redirected to a local id
|
||||||
|
@ -29,12 +31,19 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
...to,
|
...to,
|
||||||
params: {
|
params: {
|
||||||
...to.params,
|
...to.params,
|
||||||
server: currentUser.value.server,
|
server: user.server,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If we're already on an account page, we can search for this on the new instance
|
||||||
|
if (to.params.account) {
|
||||||
|
const account = await fetchAccountByHandle(to.params.account as string)
|
||||||
|
if (account)
|
||||||
|
return getAccountRoute(account)
|
||||||
|
}
|
||||||
|
|
||||||
// If we're logged in, search for the local id the account or status corresponds to
|
// If we're logged in, search for the local id the account or status corresponds to
|
||||||
const { value } = await useMasto().search({ q: `https:/${to.fullPath}`, resolve: true, limit: 1 }).next()
|
const { value } = await useMasto().search({ q: `https:/${to.fullPath}`, resolve: true, limit: 1 }).next()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import Inspect from 'vite-plugin-inspect'
|
import Inspect from 'vite-plugin-inspect'
|
||||||
import { isCI, isDevelopment } from 'std-env'
|
import { isCI } from 'std-env'
|
||||||
import { i18n } from './config/i18n'
|
import { i18n } from './config/i18n'
|
||||||
|
|
||||||
const isPreview = process.env.PULL_REQUEST === 'true'
|
const isPreview = process.env.PULL_REQUEST === 'true'
|
||||||
|
@ -36,7 +36,7 @@ export default defineNuxtConfig({
|
||||||
'import.meta.env.__BUILD_TIME__': JSON.stringify(new Date().toISOString()),
|
'import.meta.env.__BUILD_TIME__': JSON.stringify(new Date().toISOString()),
|
||||||
'import.meta.env.__BUILD_COMMIT__': JSON.stringify(process.env.COMMIT_REF || ''),
|
'import.meta.env.__BUILD_COMMIT__': JSON.stringify(process.env.COMMIT_REF || ''),
|
||||||
'process.env.VSCODE_TEXTMATE_DEBUG': 'false',
|
'process.env.VSCODE_TEXTMATE_DEBUG': 'false',
|
||||||
'process.mock': ((isDevelopment || (isCI && process.env.PULL_REQUEST === 'true')) && process.env.MOCK_USER) || 'false',
|
'process.mock': ((!isCI || isPreview) && process.env.MOCK_USER) || 'false',
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { ComponentPublicInstance } from 'vue'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
name: 'status',
|
name: 'status',
|
||||||
|
key: route => route.path,
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
key: route => `${route.params.server}:${route.params.account}`,
|
||||||
|
})
|
||||||
|
|
||||||
const params = useRoute().params
|
const params = useRoute().params
|
||||||
const accountName = $(computedEager(() => toShortHandle(params.account as string)))
|
const accountName = $(computedEager(() => toShortHandle(params.account as string)))
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ definePageMeta({
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const paginatorAll = useMasto().notifications.iterate()
|
// Default limit is 20 notifications, and servers are normally caped to 30
|
||||||
const paginatorMention = useMasto().notifications.iterate({ types: ['mention'] })
|
const paginatorAll = useMasto().notifications.iterate({ limit: 30 })
|
||||||
|
const paginatorMention = useMasto().notifications.iterate({ limit: 30, types: ['mention'] })
|
||||||
|
|
||||||
const { clearNotifications } = useNotifications()
|
const { clearNotifications } = useNotifications()
|
||||||
onActivated(clearNotifications)
|
onActivated(clearNotifications)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AccountCredentials, Emoji, Instance, Notification } from 'masto'
|
import type { Account, AccountCredentials, Emoji, Instance, Notification, Status } from 'masto'
|
||||||
|
|
||||||
export interface AppInfo {
|
export interface AppInfo {
|
||||||
id: string
|
id: string
|
||||||
|
@ -26,8 +26,23 @@ export interface ServerInfo extends Instance {
|
||||||
|
|
||||||
export interface GroupedNotifications {
|
export interface GroupedNotifications {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: Exclude<string, 'grouped-reblogs-and-favourites'>
|
||||||
items: Notification[]
|
items: Notification[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupedAccountLike {
|
||||||
|
account: Account
|
||||||
|
favourite?: Notification
|
||||||
|
reblog?: Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupedLikeNotifications {
|
||||||
|
id: string
|
||||||
|
type: 'grouped-reblogs-and-favourites'
|
||||||
|
status: Status
|
||||||
|
likes: GroupedAccountLike[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationSlot = GroupedNotifications | GroupedLikeNotifications | Notification
|
||||||
|
|
||||||
export type TranslateFn = ReturnType<typeof useI18n>['t']
|
export type TranslateFn = ReturnType<typeof useI18n>['t']
|
||||||
|
|
Loading…
Reference in a new issue