Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
9 changes: 9 additions & 0 deletions app/components/account/AccountTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const tabs = computed<CommonRouteTabOption[]>(() => [
display: t('tab.media'),
icon: 'i-ri:camera-2-line',
},
{
name: 'account-collections',
to: {
name: 'account-collections',
params: { server: server.value, account: account.value },
},
display: t('tab.collections'),
icon: 'i-ri:shapes-line',
},
])
</script>

Expand Down
2 changes: 2 additions & 0 deletions app/components/nav/NavBottom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { NavButtonName } from '../../composables/settings'

import {
NavButtonBookmark,
NavButtonCollection,
NavButtonCompose,
NavButtonExplore,
NavButtonFavorite,
Expand Down Expand Up @@ -39,6 +40,7 @@ const navButtons: NavButton[] = [
{ name: 'local', component: NavButtonLocal },
{ name: 'federated', component: NavButtonFederated },
{ name: 'list', component: NavButtonList },
{ name: 'collection', component: NavButtonCollection },
{ name: 'hashtag', component: NavButtonHashtag },
{ name: 'moreMenu', component: NavButtonMoreMenu },
]
Expand Down
1 change: 1 addition & 0 deletions app/components/nav/NavSide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const exploreLink = computed(() => {
<NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
<NavSideItem :text="$t('nav.lists')" :to="isHydrated ? `/${currentServer}/lists` : '/lists'" icon="i-ri:list-check" user-only :command="command" />
<NavSideItem :text="$t('nav.hashtags')" to="/hashtags" icon="i-ri:hashtag" user-only :command="command" />
<NavSideItem :text="$t('nav.collections')" :to="isHydrated ? `/${currentServer}/collections` : '/collections'" icon="i-ri:shapes-line" user-only :command="command" />

<div class="spacer" shrink hidden sm:block />
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />
Expand Down
17 changes: 17 additions & 0 deletions app/components/nav/button/Collection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
defineProps<{
activeClass: string
}>()
</script>

<template>
<NuxtLink
to="/collections"
:aria-label="$t('nav.collections')"
:active-class="activeClass"
flex flex-row items-center place-content-center h-full flex-1
class="coarse-pointer:select-none" @click="$scrollToTop"
>
<div i-ri:shapes-line />
</NuxtLink>
</template>
62 changes: 60 additions & 2 deletions app/components/notification/NotificationCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { mastodon } from 'masto'

// Add undocumented 'annual_report' type introduced in v4.3
// ref. https://github.com/mastodon/documentation/issues/1211#:~:text=api/v1/annual_reports
type NotificationType = mastodon.v1.Notification['type'] | 'annual_report'
type Notification = Omit<mastodon.v1.Notification, 'type'> & { type: NotificationType }
type NotificationType = mastodon.v1.Notification['type'] | 'annual_report' | 'added_to_collection' | 'collection_update'
type Notification = Omit<mastodon.v1.Notification, 'type'> & { type: NotificationType } & { collection?: mastodon.v1.Collection }

const { notification } = defineProps<{
notification: Notification
Expand All @@ -26,6 +26,8 @@ const supportedNotificationTypes: NotificationType[] = [
'status',
'annual_report',
'quote',
'added_to_collection',
'collection_update',
]

// well-known emoji reactions types Elk does not support yet
Expand Down Expand Up @@ -158,5 +160,61 @@ const timeAgo = useTimeAgo(() => notification.createdAt, timeAgoOptions)
</div>
</div>
</template>
<template v-else-if="notification.type === 'added_to_collection' && notification.collection">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can reduce the big block here by collapsing to one v-else-if like so:

<template
  v-else-if="(notification.type === 'added_to_collection'
    || notification.type === 'collection_update') && notification.collection"
>
  <NuxtLink :to="getCollectionRoute(notification.collection)">
    <div flex="~ col" p4 bg-shaded gap-3>
      <div flex>
        <div i-ri:shapes-line text-xl me-3 color-blue />
        <AccountHoverWrapper :account="notification.account">
          <!-- ...unchanged... -->
        </AccountHoverWrapper>

        <!-- the only line that differs -->
        <span>{{ notification.type === 'added_to_collection'
          ? $t('notification.added_you_to_collection')
          : $t('notification.collection_updated') }}</span>

      </div>

      <!-- ...collection name + description block, unchanged... -->
    </div>
  </NuxtLink>
</template>

<NuxtLink :to="getCollectionRoute(notification.collection)">
<div flex="~ col" p4 bg-shaded gap-3>
<div flex>
<div i-ri:shapes-line text-xl me-3 color-blue />
<AccountHoverWrapper :account="notification.account">
<NuxtLink :to="getAccountRoute(notification.account)">
<AccountDisplayName
:account="notification.account" text-primary me-1 font-bold line-clamp-1 ws-pre-wrap break-all
/>
</NuxtLink>
</AccountHoverWrapper>
<span>{{ $t('notification.added_you_to_collection') }}</span>
</div>
<div flex gap-3 ps-8>
<div i-ri:shapes-line text-xl shrink-0 text-secondary />
<div flex="~ col" min-w-0>
<div font-bold truncate>
{{ notification.collection.name }}
</div>
<div v-if="notification.collection.description" text-sm text-secondary truncate>
{{ notification.collection.description }}
</div>
</div>
</div>
</div>
</NuxtLink>
</template>
<template v-else-if="notification.type === 'collection_update' && notification.collection">
<NuxtLink :to="getCollectionRoute(notification.collection)">
<div flex="~ col" p4 bg-shaded gap-3>
<div flex>
<div i-ri:shapes-line text-xl me-3 color-blue />
<AccountHoverWrapper :account="notification.account">
<NuxtLink :to="getAccountRoute(notification.account)">
<AccountDisplayName
:account="notification.account" text-primary me-1 font-bold line-clamp-1 ws-pre-wrap break-all
/>
</NuxtLink>
</AccountHoverWrapper>
<span>{{ $t('notification.collection_updated') }}</span>
</div>
<div flex gap-3 ps-8>
<div i-ri:shapes-line text-xl shrink-0 text-secondary />
<div flex="~ col" min-w-0>
<div font-bold truncate>
{{ notification.collection.name }}
</div>
<div v-if="notification.collection.description" text-sm text-secondary truncate>
{{ notification.collection.description }}
</div>
</div>
</div>
</div>
</NuxtLink>
</template>
</article>
</template>
1 change: 1 addition & 0 deletions app/components/settings/SettingsBottomNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const availableNavButtons: NavButton[] = [
{ name: 'local', label: 'nav.local', icon: 'i-ri:group-2-line' },
{ name: 'federated', label: 'nav.federated', icon: 'i-ri:earth-line' },
{ name: 'list', label: 'nav.lists', icon: 'i-ri:list-check' },
{ name: 'collection', label: 'nav.collections', icon: 'i-ri:shapes-line' },
{ name: 'hashtag', label: 'nav.hashtags', icon: 'i-ri:hashtag' },
{ name: 'moreMenu', label: 'nav.more_menu', icon: 'i-ri:more-fill' },
] as const
Expand Down
20 changes: 20 additions & 0 deletions app/composables/masto/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ export function getStatusInReplyToRoute(status: mastodon.v1.Status) {
})
}

export function getCollectionRoute(collection: mastodon.v1.Collection) {
return useRouter().resolve({
name: 'collection',
params: {
server: currentServer.value,
id: collection.id,
},
})
}

export function getAccountCollectionsRoute(account: mastodon.v1.Account) {
return useRouter().resolve({
name: 'account-collections',
params: {
server: currentServer.value,
account: extractAccountHandle(account),
},
})
}

export function navigateToStatus({ status, focusReply = false }: {
status: mastodon.v1.Status
focusReply?: boolean
Expand Down
2 changes: 1 addition & 1 deletion app/composables/settings/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type OldFontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'

export type ColorMode = 'light' | 'dark' | 'system'

export type NavButtonName = 'home' | 'search' | 'notification' | 'mention' | 'favorite' | 'bookmark' | 'compose' | 'scheduledPosts' | 'explore' | 'local' | 'federated' | 'list' | 'hashtag' | 'setting' | 'moreMenu'
export type NavButtonName = 'home' | 'search' | 'notification' | 'mention' | 'favorite' | 'bookmark' | 'compose' | 'scheduledPosts' | 'explore' | 'local' | 'federated' | 'list' | 'collection' | 'hashtag' | 'setting' | 'moreMenu'

export interface PreferencesSettings {
hideAltIndicatorOnPosts: boolean
Expand Down
68 changes: 68 additions & 0 deletions app/pages/[[server]]/@[account]/index/collections.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { mastodon } from 'masto'

definePageMeta({
name: 'account-collections',
})

const { t } = useI18n()
const params = useRoute().params
const handle = computed(() => params.account as string)

const account = await fetchAccountByHandle(handle.value)

const client = useMastoClient()

let collectionData: mastodon.v1.Collections | null = null
if (account) {
try {
collectionData = await client.v1.accounts.$select(account.id).collections.list()
}
catch {
// server may not support collections
}
}

const collections = computed(() => collectionData?.collections ?? [])

if (account) {
useHydratedHead({
title: () => `${t('nav.collections')} | ${getDisplayName(account)} (@${account.acct})`,
})
}
</script>

<template>
<div>
<AccountTabs />
<div v-if="collections.length > 0" divide-y="base">
<NuxtLink
v-for="collection in collections"
:key="collection.id"
:to="{
name: 'collection',
params: { server: params.server, id: collection.id },
}"
block p-4 hover:bg-active transition-100
>
<div flex items-center gap-3>
<div i-ri:shapes-line text-xl shrink-0 text-secondary />
<div flex="~ col" min-w-0>
<div font-bold truncate>
{{ collection.name }}
</div>
<div v-if="collection.description" text-sm text-secondary truncate>
{{ collection.description }}
</div>
<div text-sm text-secondary>
<CommonLocalizedNumber keypath="collection.item_count" :count="collection.itemCount" />
</div>
</div>
</div>
</NuxtLink>
</div>
<div v-else p-4 text-secondary text-sm text-center>
{{ $t('collection.no_collections') }}
</div>
</div>
</template>
24 changes: 24 additions & 0 deletions app/pages/[[server]]/collections.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
const route = useRoute()
const { t } = useI18n()

useHydratedHead({
title: () => t('nav.collections'),
})
</script>

<template>
<template v-if="route.name !== 'collection'">
<MainContent>
<template #title>
<MainTitle icon="i-ri:shapes-line">
{{ t('nav.collections') }}
</MainTitle>
</template>
<NuxtPage v-if="isHydrated" :key="route.fullPath" />
</MainContent>
</template>
<template v-else>
<NuxtPage v-if="isHydrated" :key="route.fullPath" />
</template>
</template>
100 changes: 100 additions & 0 deletions app/pages/[[server]]/collections/[id].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { mastodon } from 'masto'

definePageMeta({
name: 'collection',
})

const { t } = useI18n()
const params = useRoute().params
const collectionId = computed(() => params.id as string)

const client = useMastoClient()

const { data: collectionData } = await useAsyncData<mastodon.v1.CollectionWithAccounts | null>(
() => `collection-${collectionId.value}`,
() => client.v1.collections.$select(collectionId.value).fetch().catch(() => null),
{ immediate: import.meta.client, default: () => shallowRef() },
)

const accounts = computed(() => collectionData.value?.accounts ?? [])
const collection = computed(() => collectionData.value?.collection)
const ownerAccount = computed(() => accounts.value.find(a => a.id === collection.value?.accountId))

const memberAccounts = computed(() =>
accounts.value.filter(a => a.id !== (ownerAccount.value?.id ?? ''),
))

useHydratedHead({
title: t('nav.collections'),
})
</script>

<template>
<MainContent back>
<template #title>
<MainTitle>
{{ t('nav.collections') }}
</MainTitle>
</template>

<template v-if="!collectionData">
<CommonNotFound>
{{ $t('error.collection_not_found') }}
</CommonNotFound>
</template>
<template v-else-if="collection">
<div p-4 border="b base" space-y-3>
<div flex items-center gap-2 text-start text-lg font-bold>
{{ collection.name }}
</div>

<div v-if="ownerAccount" flex items-center gap-2 text-sm text-secondary>
<span mr5>{{ $t('collection.created_by') }}</span>
<AccountInlineInfo :account="ownerAccount" class="pl1" />
</div>

<div flex text-sm text-secondary gap-4>
<span>{{ $t('collection.item_count', [collection.itemCount]) }}</span>
Comment thread
shuuji3 marked this conversation as resolved.
</div>

<div v-if="collection.description" text-secondary>
{{ collection.description }}
</div>

<div v-if="collection.tag" flex items-center gap-2>
<NuxtLink :to="getTagRoute(collection.tag.name)" text-primary text-sm font-bold hover:underline>
#{{ collection.tag.name }}
</NuxtLink>
</div>
</div>

<div v-if="memberAccounts.length > 0" divide-y="base">
<div
v-for="account in memberAccounts"
:key="account.id"
py3 px4 space-y-2
>
<div flex justify-between items-center>
<AccountInfo
:account="account"
hover-card
as="router-link"
shrink overflow-hidden
:to="getAccountRoute(account)"
/>
<AccountFollowButton :account="account" />
</div>
<div v-if="account.note" text-sm text-secondary>
<ContentRich
:content="account.note" :emojis="account.emojis"
/>
</div>
</div>
</div>
<div v-else p-4 text-secondary text-sm text-center>
{{ $t('collection.empty') }}
</div>
</template>
</MainContent>
</template>
Loading