Merge branch 'main' into fix/avatar-outline

This commit is contained in:
Daniel Roe 2023-01-08 14:16:30 +00:00 committed by GitHub
commit 30a4bed80f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
162 changed files with 1140 additions and 896 deletions

View file

@ -1,4 +1,5 @@
NUXT_PUBLIC_TRANSLATE_API= NUXT_PUBLIC_TRANSLATE_API=
NUXT_PUBLIC_DEFAULT_SERVER=
# Production only # Production only
NUXT_CLOUDFLARE_ACCOUNT_ID= NUXT_CLOUDFLARE_ACCOUNT_ID=

View file

@ -2,5 +2,6 @@
*.png *.png
*.ico *.ico
*.toml *.toml
*.patch
https-dev-config/localhost.crt https-dev-config/localhost.crt
https-dev-config/localhost.key https-dev-config/localhost.key

View file

@ -141,6 +141,7 @@ This is the full list of entries that will be available for number formatting in
- `account.followers_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use** - `account.followers_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**
- `account.following_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use** - `account.following_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**
- `account.posts_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use** - `account.posts_count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**
- `compose.drafts`: `{v}` for formatted number and `{n}` for raw number - **{v} should be use**
- `notification.followed_you_count`: `{followers}` for formatted number and `{n}` for raw number - **{followers} should be use** - `notification.followed_you_count`: `{followers}` for formatted number and `{n}` for raw number - **{followers} should be use**
- `status.poll.count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use** - `status.poll.count`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**
- `time_ago_options.*`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**: since numbers will be always small, we can also use `{n}` - `time_ago_options.*`: `{0}` for formatted number and `{n}` for raw number - **{0} should be use**: since numbers will be always small, we can also use `{n}`

View file

@ -17,7 +17,12 @@
It is already quite usable, but it isn't ready for wide adoption yet. We recommend you to use if if you would like to help us building it. We appreciate your feedback and contributions. Check out the [Open Issues](https://github.com/elk-zone/elk/issues) and jump in the action. Join the [Elk discord server](https://chat.elk.zone) to chat with us and learn more about the project. It is already quite usable, but it isn't ready for wide adoption yet. We recommend you to use if if you would like to help us building it. We appreciate your feedback and contributions. Check out the [Open Issues](https://github.com/elk-zone/elk/issues) and jump in the action. Join the [Elk discord server](https://chat.elk.zone) to chat with us and learn more about the project.
The client is deployed to [elk.zone](https://elk.zone), you can share screenshots on social media but we prefer you avoid sharing this URL directly until the app is more polished. Feel free to share the URL with your friedns and invite others you think could be interested in helping to improve Elk. The client is deployed on:
- 🦌 Production: [elk.zone](https://elk.zone)
- 🐙 Canary: [main.elk.zone](https://main.elk.zone) (deploys on every commit to `main` branch)
You can share screenshots on social media but we prefer you avoid sharing this URL directly until the app is more polished. Feel free to share the URL with your friends and invite others you think could be interested in helping to improve Elk.
## Sponsors ## Sponsors
@ -41,6 +46,10 @@ And all the companies and individuals sponsoring Elk Team members. If you're enj
We would also appreciate sponsoring other contributors to the Elk project. If someone helps you solve an issue or implement a feature you wanted, supporting them would help make this project and OS more sustainable. We would also appreciate sponsoring other contributors to the Elk project. If someone helps you solve an issue or implement a feature you wanted, supporting them would help make this project and OS more sustainable.
## Roadmap
[Open board on Volta](https://volta.net/elk-zone/elk)
## Contributing ## Contributing
We're really excited that you're interested in contributing to Elk! Before submitting your contribution, please read through the following guide. We're really excited that you're interested in contributing to Elk! Before submitting your contribution, please read through the following guide.

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
defineProps<{ defineProps<{
account: Account account: mastodon.v1.Account
square?: boolean square?: boolean
}>() }>()

View file

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
// Avatar with a background base achieving a 3px border to be used in status cards // Avatar with a background base achieving a 3px border to be used in status cards
// The border is used for Avatar on Avatar for reblogs and connecting replies // The border is used for Avatar on Avatar for reblogs and connecting replies
defineProps<{ defineProps<{
account: Account account: mastodon.v1.Account
square?: boolean square?: boolean
}>() }>()
</script> </script>
<template> <template>
<div :key="account.avatar" v-bind="$attrs" :class="{ 'rounded-full bg-base': !square }" w-54px h-54px flex items-center justify-center> <div :key="account.avatar" v-bind="$attrs" :style="{ 'clip-path': square ? `url(#avatar-mask)` : 'none' }" :class="{ 'rounded-full bg-base': !square }" w-54px h-54px flex items-center justify-center>
<AccountAvatar :account="account" w-48px h-48px :square="square" /> <AccountAvatar :account="account" w-48px h-48px :square="square" />
</div> </div>
</template> </template>

View file

@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account, as = 'div' } = $defineProps<{ const { account, as = 'div' } = $defineProps<{
account: Account account: mastodon.v1.Account
as?: string as?: string
}>() }>()

View file

@ -1,5 +1,21 @@
<script setup lang="ts">
defineProps<{
showLabel?: boolean
}>()
</script>
<template> <template>
<div flex="~" items-center border="~ base" text-secondary-light rounded-md px-1 text-xs my-auto> <div
{{ $t('account.bot') }} flex="~ gap1" items-center
:class="{ 'border border-base rounded-md px-1': showLabel }"
text-secondary-light
>
<slot name="prepend" />
<CommonTooltip :content="$t('account.bot')" :disabled="showLabel">
<div i-ri:robot-line />
</CommonTooltip>
<div v-if="showLabel">
{{ $t('account.bot') }}
</div>
</div> </div>
</template> </template>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: mastodon.v1.Account
hoverCard?: boolean hoverCard?: boolean
}>() }>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
defineProps<{ defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
</script> </script>

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account, Relationship } from 'masto' import type { mastodon } from 'masto'
const { account, command, ...props } = defineProps<{ const { account, command, ...props } = defineProps<{
account: Account account: mastodon.v1.Account
relationship?: Relationship relationship?: mastodon.v1.Relationship
command?: boolean command?: boolean
}>() }>()
@ -15,7 +15,7 @@ const masto = useMasto()
async function toggleFollow() { async function toggleFollow() {
relationship!.following = !relationship!.following relationship!.following = !relationship!.following
try { try {
const newRel = await masto.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id) const newRel = await masto.v1.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch { catch {
@ -27,7 +27,7 @@ async function toggleFollow() {
async function unblock() { async function unblock() {
relationship!.blocking = false relationship!.blocking = false
try { try {
const newRel = await masto.accounts.unblock(account.id) const newRel = await masto.v1.accounts.unblock(account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch { catch {
@ -39,7 +39,7 @@ async function unblock() {
async function unmute() { async function unmute() {
relationship!.muting = false relationship!.muting = false
try { try {
const newRel = await masto.accounts.unmute(account.id) const newRel = await masto.v1.accounts.unmute(account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch { catch {

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
const serverName = $computed(() => getServerName(account)) const serverName = $computed(() => getServerName(account))

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account, Field } from 'masto' import type { mastodon } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: mastodon.v1.Account
command?: boolean command?: boolean
}>() }>()
@ -14,8 +14,8 @@ const createdAt = $(useFormattedDateTime(() => account.createdAt, {
year: 'numeric', year: 'numeric',
})) }))
const namedFields = ref<Field[]>([]) const namedFields = ref<mastodon.v1.AccountField[]>([])
const iconFields = ref<Field[]>([]) const iconFields = ref<mastodon.v1.AccountField[]>([])
function getFieldIconTitle(fieldName: string) { function getFieldIconTitle(fieldName: string) {
return fieldName === 'Joined' ? t('account.joined') : fieldName return fieldName === 'Joined' ? t('account.joined') : fieldName
@ -40,8 +40,8 @@ function previewAvatar() {
} }
watchEffect(() => { watchEffect(() => {
const named: Field[] = [] const named: mastodon.v1.AccountField[] = []
const icons: Field[] = [] const icons: mastodon.v1.AccountField[] = []
account.fields?.forEach((field) => { account.fields?.forEach((field) => {
const icon = getAccountFieldIcon(field.name) const icon = getAccountFieldIcon(field.name)
@ -76,7 +76,7 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
<div flex="~ col gap1"> <div flex="~ col gap1">
<div flex justify-between> <div flex justify-between>
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl /> <AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
<AccountBotIndicator v-if="account.bot" /> <AccountBotIndicator v-if="account.bot" show-label />
</div> </div>
<AccountHandle :account="account" /> <AccountHandle :account="account" />
</div> </div>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
const relationship = $(useRelationship(account)) const relationship = $(useRelationship(account))

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
account?: Account account?: mastodon.v1.Account
handle?: string handle?: string
disabled?: boolean disabled?: boolean
}>() }>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account, as = 'div' } = defineProps<{ const { account, as = 'div' } = defineProps<{
account: Account account: mastodon.v1.Account
as?: string as?: string
hoverCard?: boolean hoverCard?: boolean
square?: boolean square?: boolean
@ -23,7 +23,7 @@ defineOptions({
<div flex="~ col" shrink pt-1 h-full overflow-hidden justify-center leading-none> <div flex="~ col" shrink pt-1 h-full overflow-hidden justify-center leading-none>
<div flex="~" gap-2> <div flex="~" gap-2>
<AccountDisplayName :account="account" font-bold line-clamp-1 ws-pre-wrap break-all text-lg /> <AccountDisplayName :account="account" font-bold line-clamp-1 ws-pre-wrap break-all text-lg />
<AccountBotIndicator v-if="account.bot" /> <AccountBotIndicator v-if="account.bot" text-xs />
</div> </div>
<AccountHandle :account="account" text-secondary-light /> <AccountHandle :account="account" text-secondary-light />
</div> </div>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { link = true, avatar = true } = defineProps<{ const { link = true, avatar = true } = defineProps<{
account: Account account: mastodon.v1.Account
link?: boolean link?: boolean
avatar?: boolean avatar?: boolean
}>() }>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: mastodon.v1.Account
command?: boolean command?: boolean
}>() }>()
let relationship = $(useRelationship(account)) let relationship = $(useRelationship(account))
@ -15,24 +15,31 @@ const toggleMute = async () => {
relationship!.muting = !relationship!.muting relationship!.muting = !relationship!.muting
relationship = relationship!.muting relationship = relationship!.muting
? await masto.accounts.mute(account.id, { ? await masto.v1.accounts.mute(account.id, {
// TODO support more options // TODO support more options
}) })
: await masto.accounts.unmute(account.id) : await masto.v1.accounts.unmute(account.id)
} }
const toggleBlockUser = async () => { const toggleBlockUser = async () => {
// TODO: Add confirmation // TODO: Add confirmation
relationship!.blocking = !relationship!.blocking relationship!.blocking = !relationship!.blocking
relationship = await masto.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id) relationship = await masto.v1.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id)
} }
const toggleBlockDomain = async () => { const toggleBlockDomain = async () => {
// TODO: Add confirmation // TODO: Add confirmation
relationship!.domainBlocking = !relationship!.domainBlocking relationship!.domainBlocking = !relationship!.domainBlocking
await masto.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account)) await masto.v1.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account))
}
const toggleReblogs = async () => {
// TODO: Add confirmation
const showingReblogs = !relationship?.showingReblogs
relationship = await masto.v1.accounts.follow(account.id, { reblogs: showingReblogs })
} }
</script> </script>
@ -68,6 +75,21 @@ const toggleBlockDomain = async () => {
@click="directMessageUser(account)" @click="directMessageUser(account)"
/> />
<CommonDropdownItem
v-if="!relationship?.showingReblogs"
icon="i-ri:repeat-line"
:text="$t('menu.show_reblogs', [`@${account.acct}`])"
:command="command"
@click="toggleReblogs"
/>
<CommonDropdownItem
v-else
:text="$t('menu.hide_reblogs', [`@${account.acct}`])"
icon="i-ri:repeat-line"
:command="command"
@click="toggleReblogs"
/>
<CommonDropdownItem <CommonDropdownItem
v-if="!relationship?.muting" v-if="!relationship?.muting"
:text="$t('menu.mute_account', [`@${account.acct}`])" :text="$t('menu.mute_account', [`@${account.acct}`])"

View file

@ -1,10 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
// type used in <template> import type { mastodon } from 'masto'
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { Account } from 'masto'
defineProps<{ defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
</script> </script>
@ -16,9 +14,8 @@ defineProps<{
</div> </div>
<div flex> <div flex>
<!-- type error of masto.js --> <NuxtLink :to="getAccountRoute(account.moved!)">
<NuxtLink :to="getAccountRoute(account.moved as unknown as Account)"> <AccountInfo :account="account.moved!" />
<AccountInfo :account="account.moved as unknown as Account" />
</NuxtLink> </NuxtLink>
<div flex-auto /> <div flex-auto />
<div flex items-center> <div flex items-center>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account, Paginator } from 'masto' import type { Paginator, mastodon } from 'masto'
const { paginator } = defineProps<{ const { paginator } = defineProps<{
paginator: Paginator<any, Account[]> paginator: Paginator<mastodon.v1.Account[], mastodon.DefaultPaginationParams>
}>() }>()
</script> </script>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
const { formatHumanReadableNumber, formatNumber, forSR } = useHumanReadableNumber() const { formatHumanReadableNumber, formatNumber, forSR } = useHumanReadableNumber()

View file

@ -22,7 +22,7 @@ const tabs = $computed(() => [
params: { server, account }, params: { server, account },
}, },
display: t('tab.posts_with_replies'), display: t('tab.posts_with_replies'),
icon: 'i-ri:chat-3-line', icon: 'i-ri:chat-1-line',
}, },
{ {
name: 'account-media', name: 'account-media',

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ResolvedCommand } from '@/composables/command' import type { ResolvedCommand } from '~/composables/command'
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'activate'): void (event: 'activate'): void

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AccountResult, HashTagResult, SearchResult as SearchResultType } from '@/components/search/types' import type { SearchResult as SearchResultType } from '~/composables/masto/search'
import type { CommandScope, QueryResult, QueryResultItem } from '@/composables/command' import type { CommandScope, QueryResult, QueryResultItem } from '~/composables/command'
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'close'): void (event: 'close'): void
@ -39,22 +39,8 @@ const searchResult = $computed<QueryResult>(() => {
// TODO extract this scope // TODO extract this scope
// duplicate in SearchWidget.vue // duplicate in SearchWidget.vue
const hashtagList = hashtags.value.slice(0, 3) const hashtagList = hashtags.value.slice(0, 3).map(toSearchQueryResultItem)
.map<HashTagResult>(hashtag => ({ const accountList = accounts.value.map(toSearchQueryResultItem)
type: 'hashtag',
id: hashtag.id,
hashtag,
to: getTagRoute(hashtag.name),
}))
.map(toSearchQueryResultItem)
const accountList = accounts.value
.map<AccountResult>(account => ({
type: 'account',
id: account.id,
account,
to: getAccountRoute(account),
}))
.map(toSearchQueryResultItem)
const grouped: QueryResult['grouped'] = new Map() const grouped: QueryResult['grouped'] = new Map()
grouped.set('Hashtags', hashtagList) grouped.set('Hashtags', hashtagList)

View file

@ -20,7 +20,7 @@ function close() {
<div> <div>
<slot /> <slot />
</div> </div>
<button text-xl hover:text-primary bg-hover-overflow w-1.4em h-1.4em @click="close()"> <button text-xl hover:text-primary bg-hover-overflow w="1.4em" h="1.4em" @click="close()">
<div i-ri:close-line /> <div i-ri:close-line />
</button> </button>
</div> </div>

View file

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup lang="ts" generic="T, O">
// @ts-expect-error missing types // @ts-expect-error missing types
import { DynamicScroller } from 'vue-virtual-scroller' import { DynamicScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
@ -12,20 +12,25 @@ const {
eventType = 'update', eventType = 'update',
preprocess, preprocess,
} = defineProps<{ } = defineProps<{
paginator: Paginator<any, any[]> paginator: Paginator<T[], O>
keyProp?: string keyProp?: keyof T
virtualScroller?: boolean virtualScroller?: boolean
stream?: Promise<WsEvents> stream?: Promise<WsEvents>
eventType?: 'notification' | 'update' eventType?: 'notification' | 'update'
preprocess?: (items: any[]) => any[] preprocess?: (items: T[]) => any[]
}>() }>()
defineSlots<{ defineSlots<{
default: { default: {
item: any items: T[]
item: T
index: number
active?: boolean active?: boolean
older?: any older?: T
newer?: any // newer is undefined when index === 0 newer?: T // newer is undefined when index === 0
}
items: {
items: T[]
} }
updater: { updater: {
number: number number: number
@ -35,6 +40,8 @@ defineSlots<{
done: {} done: {}
}>() }>()
const { t } = useI18n()
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream, eventType, preprocess) const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream, eventType, preprocess)
</script> </script>
@ -56,16 +63,20 @@ const { items, prevItems, update, state, endAnchor, error } = usePaginator(pagin
:active="active" :active="active"
:older="items[index + 1]" :older="items[index + 1]"
:newer="items[index - 1]" :newer="items[index - 1]"
:index="index"
:items="items"
/> />
</DynamicScroller> </DynamicScroller>
</template> </template>
<template v-else> <template v-else>
<slot <slot
v-for="item, index of items" v-for="item, index of items"
:key="item[keyProp]" :key="(item as any)[keyProp]"
:item="item" :item="item"
:older="items[index + 1]" :older="items[index + 1]"
:newer="items[index - 1]" :newer="items[index - 1]"
:index="index"
:items="items"
/> />
</template> </template>
</slot> </slot>
@ -75,11 +86,11 @@ const { items, prevItems, update, state, endAnchor, error } = usePaginator(pagin
</slot> </slot>
<slot v-else-if="state === 'done'" name="done"> <slot v-else-if="state === 'done'" name="done">
<div p5 text-secondary italic text-center> <div p5 text-secondary italic text-center>
{{ $t('common.end_of_list') }} {{ t('common.end_of_list') }}
</div> </div>
</slot> </slot>
<div v-else-if="state === 'error'" p5 text-secondary> <div v-else-if="state === 'error'" p5 text-secondary>
{{ $t('common.error') }}: {{ error }} {{ t('common.error') }}: {{ error }}
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { History } from 'masto' import type { mastodon } from 'masto'
const { const {
history, history,
maxDay = 2, maxDay = 2,
} = $defineProps<{ } = $defineProps<{
history: History[] history: mastodon.v1.TagHistory[]
maxDay?: number maxDay?: number
}>() }>()

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { History } from 'masto' import type { mastodon } from 'masto'
import sparkline from '@fnando/sparkline' import sparkline from '@fnando/sparkline'
const { const {
@ -7,7 +7,7 @@ const {
width = 60, width = 60,
height = 40, height = 40,
} = $defineProps<{ } = $defineProps<{
history?: History[] history?: mastodon.v1.TagHistory[]
width?: number width?: number
height?: number height?: number
}>() }>()

View file

@ -1,4 +1,4 @@
import type { Emoji } from 'masto' import type { mastodon } from 'masto'
defineOptions({ defineOptions({
name: 'ContentRich', name: 'ContentRich',
@ -10,7 +10,7 @@ const {
markdown = true, markdown = true,
} = defineProps<{ } = defineProps<{
content: string content: string
emojis?: Emoji[] emojis?: mastodon.v1.CustomEmoji[]
markdown?: boolean markdown?: boolean
}>() }>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Conversation } from 'masto' import type { mastodon } from 'masto'
const { conversation } = defineProps<{ const { conversation } = defineProps<{
conversation: Conversation conversation: mastodon.v1.Conversation
}>() }>()
const withAccounts = $computed(() => const withAccounts = $computed(() =>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Conversation, Paginator } from 'masto' import type { Paginator, mastodon } from 'masto'
const { paginator } = defineProps<{ const { paginator } = defineProps<{
paginator: Paginator<any, Conversation[]> paginator: Paginator<mastodon.v1.Conversation[], mastodon.DefaultPaginationParams>
}>() }>()
</script> </script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
import type { ConfirmDialogChoice } from '~/types' import type { ConfirmDialogChoice } from '~/types'
import { import {
isCommandPanelOpen, isCommandPanelOpen,
@ -30,7 +30,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
} }
}) })
const handlePublished = (status: Status) => { const handlePublished = (status: mastodon.v1.Status) => {
lastPublishDialogStatus.value = status lastPublishDialogStatus.value = status
isPublishDialogOpen.value = false isPublishDialogOpen.value = false
} }

View file

@ -40,14 +40,14 @@ onUnmounted(() => locked.value = false)
<div relative h-full w-full flex pt-12 w-100vh @click="onClick"> <div relative h-full w-full flex pt-12 w-100vh @click="onClick">
<button <button
v-if="hasNext" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.previous')" v-if="hasNext" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.previous')"
hover:bg="black/40" dark:bg="white/30" dark:hover:bg="white/20" absolute top="1/2" right-1 z5 hover:bg="black/40" dark:bg="white/30" dark-hover:bg="white/20" absolute top="1/2" right-1 z5
:title="$t('action.next')" @click="next" :title="$t('action.next')" @click="next"
> >
<div i-ri:arrow-right-s-line text-white /> <div i-ri:arrow-right-s-line text-white />
</button> </button>
<button <button
v-if="hasPrev" pointer-events-auto btn-action-icon bg="black/20" aria-label="action.next" v-if="hasPrev" pointer-events-auto btn-action-icon bg="black/20" aria-label="action.next"
hover:bg="black/40" dark:bg="white/30" dark:hover:bg="white/20" absolute top="1/2" left-1 z5 hover:bg="black/40" dark:bg="white/30" dark:hover-bg="white/20" absolute top="1/2" left-1 z5
:title="$t('action.prev')" @click="prev" :title="$t('action.prev')" @click="prev"
> >
<div i-ri:arrow-left-s-line text-white /> <div i-ri:arrow-left-s-line text-white />
@ -60,7 +60,7 @@ onUnmounted(() => locked.value = false)
<div absolute top-0 w-full flex justify-between> <div absolute top-0 w-full flex justify-between>
<button <button
btn-action-icon bg="black/30" aria-label="action.close" hover:bg="black/40" dark:bg="white/30" btn-action-icon bg="black/30" aria-label="action.close" hover:bg="black/40" dark:bg="white/30"
dark:hover:bg="white/20" pointer-events-auto shrink-0 @click="emit('close')" dark:hover-bg="white/20" pointer-events-auto shrink-0 @click="emit('close')"
> >
<div i-ri:close-line text-white /> <div i-ri:close-line text-white />
</button> </button>

View file

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { SwipeDirection } from '@vueuse/core' import { SwipeDirection } from '@vueuse/core'
import { useReducedMotion } from '@vueuse/motion' import { useReducedMotion } from '@vueuse/motion'
import type { Attachment } from 'masto' import type { mastodon } from 'masto'
const { media = [], threshold = 20 } = defineProps<{ const { media = [], threshold = 20 } = defineProps<{
media?: Attachment[] media?: mastodon.v1.MediaAttachment[]
threshold?: number threshold?: number
}>() }>()

View file

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { buildInfo } from 'virtual:build-info' const buildInfo = useRuntimeConfig().public.buildInfo
const timeAgoOptions = useTimeAgoOptions() const timeAgoOptions = useTimeAgoOptions()
const buildTimeDate = new Date(buildInfo.time) const buildTimeDate = new Date(buildInfo.time)
@ -16,7 +15,7 @@ function toggleDark() {
<footer p4 text-sm text-secondary-light flex="~ col"> <footer p4 text-sm text-secondary-light flex="~ col">
<div flex="~ gap2" items-center mb4> <div flex="~ gap2" items-center mb4>
<CommonTooltip :content="$t('nav.toggle_theme')"> <CommonTooltip :content="$t('nav.toggle_theme')">
<button flex i-ri:sun-line dark:i-ri:moon-line text-lg :aria-label="$t('nav.toggle_theme')" @click="toggleDark()" /> <button flex i-ri:sun-line dark-i-ri:moon-line text-lg :aria-label="$t('nav.toggle_theme')" @click="toggleDark()" />
</CommonTooltip> </CommonTooltip>
<CommonTooltip :content="$t('nav.zen_mode')"> <CommonTooltip :content="$t('nav.zen_mode')">
<button <button

View file

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { buildInfo } from 'virtual:build-info' const { env } = useBuildInfo()
const { env } = buildInfo
</script> </script>
<template> <template>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Notification } from 'masto' import type { mastodon } from 'masto'
const { notification } = defineProps<{ const { notification } = defineProps<{
notification: Notification notification: mastodon.v1.Notification
}>() }>()
</script> </script>

View file

@ -1,20 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
// type used in <template> import { mastodon } from 'masto'
// eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { Paginator, WsEvents } from 'masto'
import type { Notification, Paginator, WsEvents } from 'masto'
// type used in <template> // type used in <template>
// eslint-disable-next-line @typescript-eslint/consistent-type-imports // eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { GroupedAccountLike, GroupedLikeNotifications, NotificationSlot } from '~/types' import type { GroupedAccountLike, GroupedLikeNotifications, NotificationSlot } from '~/types'
const { paginator, stream } = defineProps<{ const { paginator, stream } = defineProps<{
paginator: Paginator<any, Notification[]> paginator: Paginator<mastodon.v1.Notification[], mastodon.v1.ListNotificationsParams>
stream?: Promise<WsEvents> stream?: Promise<WsEvents>
}>() }>()
const groupCapacity = Number.MAX_VALUE // No limit const groupCapacity = Number.MAX_VALUE // No limit
// Group by type (and status when applicable) // Group by type (and status when applicable)
const groupId = (item: Notification): string => { const groupId = (item: mastodon.v1.Notification): string => {
// If the update is related to an status, group notifications from the same account (boost + favorite the same status) // If the update is related to an status, group notifications from the same account (boost + favorite the same status)
const id = item.status const id = item.status
? { ? {
@ -27,12 +26,12 @@ const groupId = (item: Notification): string => {
return JSON.stringify(id) return JSON.stringify(id)
} }
function groupItems(items: Notification[]): NotificationSlot[] { function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] {
const results: NotificationSlot[] = [] const results: NotificationSlot[] = []
let id = 0 let id = 0
let currentGroupId = '' let currentGroupId = ''
let currentGroup: Notification[] = [] let currentGroup: mastodon.v1.Notification[] = []
const processGroup = () => { const processGroup = () => {
if (currentGroup.length === 0) if (currentGroup.length === 0)
return return
@ -127,7 +126,7 @@ const { formatNumber } = useHumanReadableNumber()
/> />
<NotificationCard <NotificationCard
v-else v-else
:notification="item as Notification" :notification="item as mastodon.v1.Notification"
hover:bg-active hover:bg-active
border="b base" border="b base"
/> />

View file

@ -31,7 +31,7 @@ const { modelValue } = defineModel<{
:aria-label="$t('settings.notifications.push_notifications.subscription_error.clear_error')" :aria-label="$t('settings.notifications.push_notifications.subscription_error.clear_error')"
@click="modelValue = false" @click="modelValue = false"
> >
<span aria-hidden="true" w-1.75em h-1.75em i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </head>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Attachment } from 'masto' import type { mastodon } from 'masto'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
attachment: Attachment attachment: mastodon.v1.MediaAttachment
alt?: string alt?: string
removable?: boolean removable?: boolean
dialogLabelledBy?: string dialogLabelledBy?: string

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Attachment, CreateStatusParams, Status, StatusVisibility } from 'masto' import type { mastodon } from 'masto'
import { fileOpen } from 'browser-fs-access' import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core' import { useDropZone } from '@vueuse/core'
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
@ -18,13 +18,13 @@ const {
initial?: () => Draft initial?: () => Draft
placeholder?: string placeholder?: string
inReplyToId?: string inReplyToId?: string
inReplyToVisibility?: StatusVisibility inReplyToVisibility?: mastodon.v1.StatusVisibility
expanded?: boolean expanded?: boolean
dialogLabelledBy?: string dialogLabelledBy?: string
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(evt: 'published', status: Status): void (evt: 'published', status: mastodon.v1.Status): void
}>() }>()
const { t } = useI18n() const { t } = useI18n()
@ -103,7 +103,7 @@ async function uploadAttachments(files: File[]) {
if (draft.attachments.length < limit) { if (draft.attachments.length < limit) {
isExceedingAttachmentLimit = false isExceedingAttachmentLimit = false
try { try {
const attachment = await masto.mediaAttachments.create({ const attachment = await masto.v1.mediaAttachments.create({
file, file,
}) })
draft.attachments.push(attachment) draft.attachments.push(attachment)
@ -122,9 +122,9 @@ async function uploadAttachments(files: File[]) {
isUploading = false isUploading = false
} }
async function setDescription(att: Attachment, description: string) { async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
att.description = description att.description = description
await masto.mediaAttachments.update(att.id, { description: att.description }) await masto.v1.mediaAttachments.update(att.id, { description: att.description })
} }
function removeAttachment(index: number) { function removeAttachment(index: number) {
@ -136,8 +136,8 @@ async function publish() {
...draft.params, ...draft.params,
status: htmlToText(draft.params.status || ''), status: htmlToText(draft.params.status || ''),
mediaIds: draft.attachments.map(a => a.id), mediaIds: draft.attachments.map(a => a.id),
...(masto.version.includes('+glitch') ? { 'content-type': 'text/markdown' } : {}), ...((masto.config as any).props.version.raw.includes('+glitch') ? { 'content-type': 'text/markdown' } : {}),
} as CreateStatusParams } as mastodon.v1.CreateStatusParams
if (process.dev) { if (process.dev) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -154,11 +154,13 @@ async function publish() {
try { try {
isSending = true isSending = true
let status: Status let status: mastodon.v1.Status
if (!draft.editingStatus) if (!draft.editingStatus)
status = await masto.statuses.create(payload) status = await masto.v1.statuses.create(payload)
else else
status = await masto.statuses.update(draft.editingStatus.id, payload) status = await masto.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId)
navigateToStatus({ status })
draft = initial() draft = initial()
emit('published', status) emit('published', status)
@ -251,7 +253,7 @@ defineExpose({
:aria-label="$t('action.clear_upload_failed')" :aria-label="$t('action.clear_upload_failed')"
@click="failed = []" @click="failed = []"
> >
<span aria-hidden="true" w-1.75em h-1.75em i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </head>
@ -313,7 +315,7 @@ defineExpose({
<div flex-auto /> <div flex-auto />
<div dir="ltr" pointer-events-none pe-1 pt-2 text-sm tabular-nums text-secondary flex gap-0.5 :class="{ 'text-rose-500': characterCount > characterLimit }"> <div dir="ltr" pointer-events-none pe-1 pt-2 text-sm tabular-nums text-secondary flex gap="0.5" :class="{ 'text-rose-500': characterCount > characterLimit }">
{{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span> {{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span>
</div> </div>

View file

@ -2,6 +2,8 @@
import { formatTimeAgo } from '@vueuse/core' import { formatTimeAgo } from '@vueuse/core'
const route = useRoute() const route = useRoute()
const { formatNumber } = useHumanReadableNumber()
const timeAgoOptions = useTimeAgoOptions()
let draftKey = $ref('home') let draftKey = $ref('home')
@ -25,7 +27,7 @@ onMounted(() => {
<div text-right h-8> <div text-right h-8>
<VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end"> <VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end">
<button btn-text flex="inline center"> <button btn-text flex="inline center">
Drafts ({{ nonEmptyDrafts.length }}) <div i-ri:arrow-down-s-line /> {{ $t('compose.drafts', nonEmptyDrafts.length, { named: { v: formatNumber(nonEmptyDrafts.length) } }) }}&#160;<div aria-hidden="true" i-ri:arrow-down-s-line />
</button> </button>
<template #popper="{ hide }"> <template #popper="{ hide }">
<div flex="~ col"> <div flex="~ col">
@ -38,9 +40,11 @@ onMounted(() => {
> >
<div> <div>
<div flex="~ gap-1" items-center> <div flex="~ gap-1" items-center>
Draft <code>{{ key }}</code> <i18n-t keypath="compose.draft_title">
<code>{{ key }}</code>
</i18n-t>
<span v-if="draft.lastUpdated" text-secondary text-sm> <span v-if="draft.lastUpdated" text-secondary text-sm>
&middot; {{ formatTimeAgo(new Date(draft.lastUpdated)) }} &middot; {{ formatTimeAgo(new Date(draft.lastUpdated), timeAgoOptions) }}
</span> </span>
</div> </div>
<div text-secondary> <div text-secondary>

View file

@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
defineProps<{ defineProps<{
account: Account account: mastodon.v1.Account
}>() }>()
</script> </script>
<template> <template>
<button flex gap-2 items-center> <div flex gap-2 items-center>
<AccountAvatar w-10 h-10 :account="account" shrink-0 /> <AccountAvatar w-10 h-10 :account="account" shrink-0 />
<div flex="~ col gap1" shrink h-full overflow-hidden leading-none> <div flex="~ col gap1" shrink h-full overflow-hidden leading-none>
<div flex="~" gap-2> <div flex="~" gap-2>
<AccountDisplayName :account="account" line-clamp-1 ws-pre-wrap break-all text-base /> <AccountDisplayName :account="account" line-clamp-1 ws-pre-wrap break-all text-base />
<AccountBotIndicator v-if="account.bot" /> <AccountBotIndicator v-if="account.bot" text-xs />
</div> </div>
<AccountHandle text-sm :account="account" text-secondary-light /> <AccountHandle text-sm :account="account" text-secondary-light />
</div> </div>
</button> </div>
</template> </template>

View file

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Tag } from 'masto' import type { mastodon } from 'masto'
const { hashtag } = defineProps<{ hashtag: Tag }>() const { hashtag } = defineProps<{
hashtag: mastodon.v1.Tag
}>()
const totalTrend = $computed(() => const totalTrend = $computed(() =>
hashtag.history?.reduce((total: number, item) => total + (Number(item.accounts) || 0), 0), hashtag.history?.reduce((total: number, item) => total + (Number(item.accounts) || 0), 0),
@ -20,7 +22,10 @@ const totalTrend = $computed(() =>
<CommonTrending :history="hashtag.history" text-xs text-secondary truncate /> <CommonTrending :history="hashtag.history" text-xs text-secondary truncate />
</div> </div>
<div v-if="totalTrend" absolute left-15 right-0 top-0 bottom-4 op35 flex place-items-center place-content-center ml-auto> <div v-if="totalTrend" absolute left-15 right-0 top-0 bottom-4 op35 flex place-items-center place-content-center ml-auto>
<CommonTrendingCharts :history="hashtag.history" text-xs text-secondary width="150" height="20" h-full w-full /> <CommonTrendingCharts
:history="hashtag.history" :width="150" :height="20"
text-xs text-secondary h-full w-full
/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SearchResult } from './types' import type { SearchResult } from '~/composables/masto/search'
defineProps<{ defineProps<{
result: SearchResult result: SearchResult
@ -21,9 +21,9 @@ const onActivate = () => {
:class="{ 'bg-active': active }" :class="{ 'bg-active': active }"
@click="() => onActivate()" @click="() => onActivate()"
> >
<SearchHashtagInfo v-if="result.type === 'hashtag'" :hashtag="result.hashtag" /> <SearchHashtagInfo v-if="result.type === 'hashtag'" :hashtag="result.data" />
<SearchAccountInfo v-else-if="result.type === 'account' && result.account" :account="result.account" /> <SearchAccountInfo v-else-if="result.type === 'account'" :account="result.data" />
<StatusCard v-else-if="result.type === 'status' && result.status" :status="result.status" :actions="false" :show-reply-to="false" /> <StatusCard v-else-if="result.type === 'status'" :status="result.data" :actions="false" :show-reply-to="false" />
<!-- <div v-else-if="result.type === 'action'" text-center> <!-- <div v-else-if="result.type === 'action'" text-center>
{{ result.action!.label }} {{ result.action!.label }}
</div> --> </div> -->

View file

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AccountResult, HashTagResult, StatusResult } from './types'
const query = ref('') const query = ref('')
const { accounts, hashtags, loading, statuses } = useSearch(query) const { accounts, hashtags, loading, statuses } = useSearch(query)
const index = ref(0) const index = ref(0)
@ -15,24 +13,9 @@ const results = computed(() => {
return [] return []
const results = [ const results = [
...hashtags.value.slice(0, 3).map<HashTagResult>(hashtag => ({ ...hashtags.value.slice(0, 3),
type: 'hashtag', ...accounts.value,
id: hashtag.id, ...statuses.value,
hashtag,
to: getTagRoute(hashtag.name),
})),
...accounts.value.map<AccountResult>(account => ({
type: 'account',
id: account.id,
account,
to: getAccountRoute(account),
})),
...statuses.value.map<StatusResult>(status => ({
type: 'status',
id: status.id,
status,
to: getStatusRoute(status),
})),
// Disable until search page is implemented // Disable until search page is implemented
// { // {
@ -53,16 +36,18 @@ watch([results, focused], () => index.value = -1)
const shift = (delta: number) => index.value = (index.value + delta % results.value.length + results.value.length) % results.value.length const shift = (delta: number) => index.value = (index.value + delta % results.value.length + results.value.length) % results.value.length
const activate = () => { const activate = () => {
(document.activeElement as HTMLElement).blur()
const currentIndex = index.value const currentIndex = index.value
index.value = -1 index.value = -1
if (query.value.length === 0) if (query.value.length === 0)
return return
(document.activeElement as HTMLElement).blur()
// Disable until search page is implemented // Disable until search page is implemented
// if (currentIndex === -1) if (currentIndex === -1)
// router.push(`/search?q=${query.value}`) // router.push(`/search?q=${query.value}`)
return
router.push(results.value[currentIndex].to) router.push(results.value[currentIndex].to)
} }

View file

@ -1,17 +0,0 @@
import type { Account, Status } from 'masto'
import type { RouteLocation } from 'vue-router'
export type BuildResult<K extends keyof any, T> = {
[P in K]: T
} & {
id: string
type: K
to: RouteLocation & {
href: string
}
}
export type HashTagResult = BuildResult<'hashtag', any>
export type AccountResult = BuildResult<'account', Account>
export type StatusResult = BuildResult<'status', Status>
export type SearchResult = HashTagResult | AccountResult | StatusResult

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { UpdateCredentialsParams } from 'masto' import type { mastodon } from 'masto'
const { form } = defineModel<{ const { form } = defineModel<{
form: { form: {
fieldsAttributes: NonNullable<UpdateCredentialsParams['fieldsAttributes']> fieldsAttributes: NonNullable<mastodon.v1.UpdateCredentialsParams['fieldsAttributes']>
} }
}>() }>()
const dropdown = $ref<any>() const dropdown = $ref<any>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
const { account, link = true } = defineProps<{ const { account, link = true } = defineProps<{
account: Account account: mastodon.v1.Account
link?: boolean link?: boolean
}>() }>()
</script> </script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const { as = 'button', command, disabled, content, icon } = defineProps<{
text?: string | number text?: string | number
content: string content: string
color: string color: string
@ -27,10 +27,10 @@ useCommand({
scope: 'Actions', scope: 'Actions',
order: -2, order: -2,
visible: () => props.command && !props.disabled, visible: () => command && !disabled,
name: () => props.content, name: () => content,
icon: () => props.icon, icon: () => icon,
onActivate() { onActivate() {
if (!checkLogin()) if (!checkLogin())
@ -47,18 +47,27 @@ useCommand({
<template> <template>
<component <component
:is="as || 'button'" :is="as"
v-bind="$attrs" ref="el" v-bind="$attrs" ref="el"
w-fit flex gap-1 items-center w-fit flex gap-1 items-center
rounded group :hover="hover" rounded group
focus:outline-none cursor-pointer :hover=" !disabled ? hover : undefined"
focus:outline-none
:focus-visible="hover" :focus-visible="hover"
:class="active ? [color] : 'text-secondary'" :class="active ? color : 'text-secondary'"
:aria-label="content" :aria-label="content"
:disabled="disabled"
> >
<CommonTooltip placement="bottom" :content="content"> <CommonTooltip placement="bottom" :content="content">
<div rounded-full p2 :group-hover="groupHover" :group-focus-visible="groupHover" group-focus-visible:ring="2 current"> <div
<div :class="[active && activeIcon ? activeIcon : icon, { 'pointer-events-none': disabled }]" /> rounded-full p2
v-bind="disabled ? {} : {
'group-hover': groupHover,
'group-focus-visible': groupHover,
'group-focus-visible:ring': '2 current',
}"
>
<div :class="active && activeIcon ? activeIcon : icon" />
</div> </div>
</CommonTooltip> </CommonTooltip>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
status: Status status: mastodon.v1.Status
details?: boolean details?: boolean
command?: boolean command?: boolean
}>() }>()
@ -14,6 +14,7 @@ const { details, command } = $(props)
const { const {
status, status,
isLoading, isLoading,
canReblog,
toggleBookmark, toggleBookmark,
toggleFavourite, toggleFavourite,
toggleReblog, toggleReblog,
@ -26,9 +27,8 @@ const reply = () => {
return return
if (details) if (details)
focusEditor() focusEditor()
else else
navigateTo({ path: getStatusRoute(status).href, state: { focusReply: true } }) navigateToStatus({ status, focusReply: true })
} }
</script> </script>
@ -39,7 +39,7 @@ const reply = () => {
:content="$t('action.reply')" :content="$t('action.reply')"
:text="status.repliesCount || ''" :text="status.repliesCount || ''"
color="text-blue" hover="text-blue" group-hover="bg-blue/10" color="text-blue" hover="text-blue" group-hover="bg-blue/10"
icon="i-ri:chat-3-line" icon="i-ri:chat-1-line"
:command="command" :command="command"
@click="reply" @click="reply"
> >
@ -63,7 +63,7 @@ const reply = () => {
icon="i-ri:repeat-line" icon="i-ri:repeat-line"
active-icon="i-ri:repeat-fill" active-icon="i-ri:repeat-fill"
:active="!!status.reblogged" :active="!!status.reblogged"
:disabled="isLoading.reblogged" :disabled="isLoading.reblogged || !canReblog"
:command="command" :command="command"
@click="toggleReblog()" @click="toggleReblog()"
> >

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
status: Status status: mastodon.v1.Status
details?: boolean details?: boolean
command?: boolean command?: boolean
}>() }>()
@ -40,21 +40,21 @@ const toggleTranslation = async () => {
const masto = useMasto() const masto = useMasto()
const getPermalinkUrl = (status: Status) => { const getPermalinkUrl = (status: mastodon.v1.Status) => {
const url = getStatusPermalinkRoute(status) const url = getStatusPermalinkRoute(status)
if (url) if (url)
return `${location.origin}/${url}` return `${location.origin}/${url}`
return null return null
} }
const copyLink = async (status: Status) => { const copyLink = async (status: mastodon.v1.Status) => {
const url = getPermalinkUrl(status) const url = getPermalinkUrl(status)
if (url) if (url)
await clipboard.copy(url) await clipboard.copy(url)
} }
const { share, isSupported: isShareSupported } = useShare() const { share, isSupported: isShareSupported } = useShare()
const shareLink = async (status: Status) => { const shareLink = async (status: mastodon.v1.Status) => {
const url = getPermalinkUrl(status) const url = getPermalinkUrl(status)
if (url) if (url)
await share({ url }) await share({ url })
@ -69,7 +69,7 @@ const deleteStatus = async () => {
return return
removeCachedStatus(status.id) removeCachedStatus(status.id)
await masto.statuses.remove(status.id) await masto.v1.statuses.remove(status.id)
if (route.name === 'status') if (route.name === 'status')
router.back() router.back()
@ -87,7 +87,7 @@ const deleteAndRedraft = async () => {
} }
removeCachedStatus(status.id) removeCachedStatus(status.id)
await masto.statuses.remove(status.id) await masto.v1.statuses.remove(status.id)
await openPublishDialog('dialog', await getDraftFromStatus(status), true) await openPublishDialog('dialog', await getDraftFromStatus(status), true)
// Go to the new status, if the page is the old status // Go to the new status, if the page is the old status
@ -129,7 +129,7 @@ async function editStatus() {
<template v-if="userSettings.zenMode"> <template v-if="userSettings.zenMode">
<CommonDropdownItem <CommonDropdownItem
:text="$t('action.reply')" :text="$t('action.reply')"
icon="i-ri:chat-3-line" icon="i-ri:chat-1-line"
:command="command" :command="command"
@click="reply()" @click="reply()"
/> />

View file

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { clamp } from '@vueuse/core' import { clamp } from '@vueuse/core'
import type { Attachment } from 'masto' import type { mastodon } from 'masto'
const { const {
attachment, attachment,
fullSize = false, fullSize = false,
} = defineProps<{ } = defineProps<{
attachment: Attachment attachment: mastodon.v1.MediaAttachment
attachments?: Attachment[] attachments?: mastodon.v1.MediaAttachment[]
fullSize?: boolean fullSize?: boolean
}>() }>()
@ -65,14 +65,23 @@ const video = ref<HTMLVideoElement | undefined>()
const prefersReducedMotion = usePreferredReducedMotion() const prefersReducedMotion = usePreferredReducedMotion()
useIntersectionObserver(video, (entries) => { useIntersectionObserver(video, (entries) => {
if (prefersReducedMotion.value === 'reduce') const ready = video.value?.dataset.ready === 'true'
if (prefersReducedMotion.value === 'reduce') {
if (ready && !video.value?.paused)
video.value?.pause()
return return
}
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.intersectionRatio <= 0.75) if (entry.intersectionRatio <= 0.75) {
!video.value!.paused && video.value!.pause() ready && !video.value?.paused && video.value?.pause()
else }
video.value!.play() else {
video.value?.play().then(() => {
video.value!.dataset.ready = 'true'
}).catch(noop)
}
}) })
}, { threshold: 0.75 }) }, { threshold: 0.75 })
</script> </script>

View file

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status, StatusEdit } from 'masto' import type { mastodon } from 'masto'
const { const {
status, status,
withAction = true, withAction = true,
} = defineProps<{ } = defineProps<{
status: Status | StatusEdit status: mastodon.v1.Status | mastodon.v1.StatusEdit
withAction?: boolean withAction?: boolean
}>() }>()

View file

@ -1,25 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FilterContext, Status } from 'masto' import type { mastodon } from 'masto'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
status: Status status: mastodon.v1.Status
actions?: boolean actions?: boolean
context?: FilterContext context?: mastodon.v2.FilterContext
hover?: boolean hover?: boolean
faded?: boolean faded?: boolean
// If we know the prev and next status in the timeline, we can simplify the card // If we know the prev and next status in the timeline, we can simplify the card
older?: Status older?: mastodon.v1.Status
newer?: Status newer?: mastodon.v1.Status
// Manual overrides // Manual overrides
hasOlder?: boolean hasOlder?: boolean
hasNewer?: boolean hasNewer?: boolean
// When looking into a detailed view of a post, we can simplify the replying badges // When looking into a detailed view of a post, we can simplify the replying badges
// to the main expanded post // to the main expanded post
main?: Status main?: mastodon.v1.Status
}>(), }>(),
{ actions: true, showReplyTo: true }, { actions: true },
) )
const status = $computed(() => { const status = $computed(() => {
@ -32,9 +33,13 @@ const status = $computed(() => {
const directReply = $computed(() => props.hasNewer || (!!status.inReplyToId && (status.inReplyToId === props.newer?.id || status.inReplyToId === props.newer?.reblog?.id))) const directReply = $computed(() => props.hasNewer || (!!status.inReplyToId && (status.inReplyToId === props.newer?.id || status.inReplyToId === props.newer?.reblog?.id)))
// Use reblogged status, connect it to further replies // Use reblogged status, connect it to further replies
const connectReply = $computed(() => props.hasOlder || status.id === props.older?.inReplyToId || status.id === props.older?.reblog?.inReplyToId) const connectReply = $computed(() => props.hasOlder || status.id === props.older?.inReplyToId || status.id === props.older?.reblog?.inReplyToId)
// Open a detailed status, the replies directly to it
const replyToMain = $computed(() => props.main && props.main.id === status.inReplyToId)
const rebloggedBy = $computed(() => props.status.reblog ? props.status.account : null) const rebloggedBy = $computed(() => props.status.reblog ? props.status.account : null)
const statusRoute = $computed(() => getStatusRoute(status))
const el = ref<HTMLElement>() const el = ref<HTMLElement>()
const router = useRouter() const router = useRouter()
@ -47,13 +52,12 @@ function onclick(evt: MouseEvent | KeyboardEvent) {
} }
function go(evt: MouseEvent | KeyboardEvent) { function go(evt: MouseEvent | KeyboardEvent) {
const route = getStatusRoute(status)
if (evt.metaKey || evt.ctrlKey) { if (evt.metaKey || evt.ctrlKey) {
window.open(route.href) window.open(statusRoute.href)
} }
else { else {
cacheStatus(status) cacheStatus(status)
router.push(route) router.push(statusRoute)
} }
} }
@ -63,24 +67,19 @@ const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
// Content Filter logic // Content Filter logic
const filterResult = $computed(() => status.filtered?.length ? status.filtered[0] : null) const filterResult = $computed(() => status.filtered?.length ? status.filtered[0] : null)
const filter = $computed(() => filterResult?.filter) const filter = $computed(() => filterResult?.filter as mastodon.v2.Filter)
// a bit of a hack due to Filter being different in v1 and v2 // a bit of a hack due to Filter being different in v1 and v2
// clean up when masto.js supports explicit versions: https://github.com/neet/masto.js/issues/722 // clean up when masto.js supports explicit versions: https://github.com/neet/masto.js/issues/722
const filterPhrase = $computed(() => filter?.phrase || (filter as any)?.title) const filterPhrase = $computed(() => filter?.phrase || (filter as any)?.title)
const isFiltered = $computed(() => filterPhrase && (props.context ? filter?.context.includes(props.context) : false)) const isFiltered = $computed(() => filterPhrase && (props.context ? filter?.context.includes(props.context) : false))
const isSelfReply = $computed(() => status.inReplyToAccountId === status.account.id)
const collapseRebloggedBy = $computed(() => rebloggedBy?.id === status.account.id) const collapseRebloggedBy = $computed(() => rebloggedBy?.id === status.account.id)
// Collapse ReplyingTo badge if it is a self-reply (thread)
const collapseReplyingTo = $computed(() => (!rebloggedBy || collapseRebloggedBy) && status.inReplyToAccountId === status.account.id)
// Only show avatar in ReplyingTo badge if it was reblogged by the same account or if it is against the main post
const simplifyReplyingTo = $computed(() =>
(props.main && props.main.account.id === status.inReplyToAccountId) || (rebloggedBy && rebloggedBy.id === status.inReplyToAccountId),
)
const isDM = $computed(() => status.visibility === 'direct') const isDM = $computed(() => status.visibility === 'direct')
const showUpperBorder = $computed(() => props.newer && !directReply)
const showReplyTo = $computed(() => !replyToMain && !directReply)
</script> </script>
<template> <template>
@ -88,8 +87,7 @@ const isDM = $computed(() => status.visibility === 'direct')
v-if="filter?.filterAction !== 'hide'" v-if="filter?.filterAction !== 'hide'"
:id="`status-${status.id}`" :id="`status-${status.id}`"
ref="el" ref="el"
relative flex flex-col gap-1 pl-3 pr-4 pt-1 relative flex="~ col gap1" p="l-3 r-4 b-2"
class="pb-1.5"
:class="{ 'hover:bg-active': hover }" :class="{ 'hover:bg-active': hover }"
tabindex="0" tabindex="0"
focus:outline-none focus-visible:ring="2 primary" focus:outline-none focus-visible:ring="2 primary"
@ -97,11 +95,38 @@ const isDM = $computed(() => status.visibility === 'direct')
@click="onclick" @click="onclick"
@keydown.enter="onclick" @keydown.enter="onclick"
> >
<div v-if="newer && !directReply" w-auto h-1px bg-border /> <!-- Upper border -->
<div flex justify-between> <div :h="showUpperBorder ? '1px' : '0'" w-auto bg-border mb-1 />
<slot name="meta">
<div v-if="rebloggedBy && !collapseRebloggedBy" relative text-secondary ws-nowrap flex="~" items-center pt1 pb0.5 px-1px bg-base> <slot name="meta">
<div i-ri:repeat-fill me-46px text-primary w-16px h-16px /> <!-- Line connecting to previous status -->
<template v-if="status.inReplyToAccountId">
<StatusReplyingTo
v-if="showReplyTo"
ml-6 pt-1 pl-5
:status="status"
:is-self-reply="isSelfReply"
:class="faded ? 'text-secondary-light' : ''"
/>
<div flex="~ col gap-1" items-center pos="absolute top-0 left-0" w="20.5" z--1>
<template v-if="showReplyTo">
<div w="1px" h="0.5" border="x base" mt-3 />
<div w="1px" h="0.5" border="x base" />
<div w="1px" h="0.5" border="x base" />
</template>
<div w="1px" h-10 border="x base" />
</div>
</template>
<!-- Reblog status -->
<div flex="~ col" justify-between>
<div
v-if="rebloggedBy && !collapseRebloggedBy"
flex="~" items-center
p="t-1 b-0.5 x-1px"
relative text-secondary ws-nowrap
>
<div i-ri:repeat-fill me-46px text-green w-16px h-16px />
<div absolute top-1 ms-24px w-32px h-32px rounded-full> <div absolute top-1 ms-24px w-32px h-32px rounded-full>
<AccountHoverWrapper :account="rebloggedBy"> <AccountHoverWrapper :account="rebloggedBy">
<NuxtLink :to="getAccountRoute(rebloggedBy)"> <NuxtLink :to="getAccountRoute(rebloggedBy)">
@ -111,38 +136,39 @@ const isDM = $computed(() => status.visibility === 'direct')
</div> </div>
<AccountInlineInfo font-bold :account="rebloggedBy" :avatar="false" text-sm /> <AccountInlineInfo font-bold :account="rebloggedBy" :avatar="false" text-sm />
</div> </div>
<div v-else /> </div>
</slot> </slot>
<StatusReplyingTo v-if="!directReply && !collapseReplyingTo" :status="status" :simplified="!!simplifyReplyingTo" :class="faded ? 'text-secondary-light' : ''" pt1 />
</div>
<div flex gap-3 :class="{ 'text-secondary': faded }"> <div flex gap-3 :class="{ 'text-secondary': faded }">
<!-- Avatar -->
<div relative> <div relative>
<div v-if="collapseRebloggedBy" absolute flex items-center justify-center top--6px px-2px py-3px rounded-full bg-base> <div v-if="collapseRebloggedBy" absolute flex items-center justify-center top--6px px-2px py-3px rounded-full bg-base>
<div i-ri:repeat-fill text-primary w-16px h-16px /> <div i-ri:repeat-fill text-green w-16px h-16px />
</div> </div>
<AccountHoverWrapper :account="status.account"> <AccountHoverWrapper :account="status.account">
<NuxtLink :to="getAccountRoute(status.account)" rounded-full> <NuxtLink :to="getAccountRoute(status.account)" rounded-full>
<AccountBigAvatar :account="status.account" /> <AccountBigAvatar :account="status.account" />
</NuxtLink> </NuxtLink>
</AccountHoverWrapper> </AccountHoverWrapper>
<div v-if="connectReply" w-full h-full flex justify-center>
<div class="w-2.5px" bg-primary-light /> <div v-if="connectReply" w-full h-full flex mt--3px justify-center>
<div w-1px border="x base" />
</div> </div>
</div> </div>
<!-- Main -->
<div flex="~ col 1" min-w-0> <div flex="~ col 1" min-w-0>
<!-- Account Info -->
<div flex items-center space-x-1> <div flex items-center space-x-1>
<AccountHoverWrapper :account="status.account"> <AccountHoverWrapper :account="status.account">
<StatusAccountDetails :account="status.account" /> <StatusAccountDetails :account="status.account" />
</AccountHoverWrapper> </AccountHoverWrapper>
<div v-if="!directReply && collapseReplyingTo" flex="~" ps-1 items-center justify-center>
<StatusReplyingTo :collapsed="true" :status="status" :class="faded ? 'text-secondary-light' : ''" />
</div>
<div flex-auto /> <div flex-auto />
<div v-if="!userSettings.zenMode" text-sm text-secondary flex="~ row nowrap" hover:underline> <div v-show="!userSettings.zenMode" text-sm text-secondary flex="~ row nowrap" hover:underline>
<AccountBotIndicator v-if="status.account.bot" me-2 /> <AccountBotIndicator v-if="status.account.bot" me-2 />
<div flex> <div flex>
<CommonTooltip :content="createdAt"> <CommonTooltip :content="createdAt">
<a :title="status.createdAt" :href="getStatusRoute(status).href" @click.prevent="go($event)"> <a :title="status.createdAt" :href="statusRoute.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">
{{ timeago }} {{ timeago }}
</time> </time>
@ -153,10 +179,10 @@ const isDM = $computed(() => status.visibility === 'direct')
</div> </div>
<StatusActionsMore v-if="actions !== false" :status="status" me--2 /> <StatusActionsMore v-if="actions !== false" :status="status" me--2 />
</div> </div>
<!-- Content -->
<StatusContent :status="status" :context="context" mb2 :class="{ 'mt-2 mb1': isDM }" /> <StatusContent :status="status" :context="context" mb2 :class="{ 'mt-2 mb1': isDM }" />
<div> <StatusActions v-if="actions !== false" v-show="!userSettings.zenMode" :status="status" />
<StatusActions v-if="(actions !== false && !userSettings.zenMode)" :status="status" />
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FilterContext, Status } from 'masto' import type { mastodon } from 'masto'
const { status, context } = defineProps<{ const { status, context } = defineProps<{
status: Status status: mastodon.v1.Status
context?: FilterContext | 'details' context?: mastodon.v2.FilterContext | 'details'
}>() }>()
const isDM = $computed(() => status.visibility === 'direct') const isDM = $computed(() => status.visibility === 'direct')

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
status: Status status: mastodon.v1.Status
command?: boolean command?: boolean
actions?: boolean actions?: boolean
}>(), { }>(), {

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status, StatusEdit } from 'masto' import type { mastodon } from 'masto'
const { status } = defineProps<{ const { status } = defineProps<{
status: Status | StatusEdit status: mastodon.v1.Status | mastodon.v1.StatusEdit
fullSize?: boolean fullSize?: boolean
}>() }>()
</script> </script>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const { status } = defineProps<{ const { status } = defineProps<{
status: Status status: mastodon.v1.Status
}>() }>()
const poll = reactive({ ...status.poll! }) const poll = reactive({ ...status.poll! })
@ -30,7 +30,7 @@ async function vote(e: Event) {
poll.votersCount = (poll.votersCount || 0) + 1 poll.votersCount = (poll.votersCount || 0) + 1
cacheStatus({ ...status, poll }, undefined, true) cacheStatus({ ...status, poll }, undefined, true)
await masto.poll.vote(poll.id, { choices }) await masto.v1.polls.vote(poll.id, { choices })
} }
const votersCount = $computed(() => poll.votersCount ?? 0) const votersCount = $computed(() => poll.votersCount ?? 0)

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Card, CardType } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
card: Card card: mastodon.v1.PreviewCard
/** For the preview image, only the small image mode is displayed */ /** For the preview image, only the small image mode is displayed */
smallPictureOnly?: boolean smallPictureOnly?: boolean
/** When it is root card in the list, not appear as a child card */ /** When it is root card in the list, not appear as a child card */
@ -24,7 +24,7 @@ const providerName = $computed(() => props.card.providerName ? props.card.provid
const gitHubCards = $(useFeatureFlag('experimentalGitHubCards')) const gitHubCards = $(useFeatureFlag('experimentalGitHubCards'))
// TODO: handle card.type: 'photo' | 'video' | 'rich'; // TODO: handle card.type: 'photo' | 'video' | 'rich';
const cardTypeIconMap: Record<CardType, string> = { const cardTypeIconMap: Record<mastodon.v1.PreviewCardType, string> = {
link: 'i-ri:profile-line', link: 'i-ri:profile-line',
photo: 'i-ri:image-line', photo: 'i-ri:image-line',
video: 'i-ri:play-line', video: 'i-ri:play-line',

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Card } from 'masto' import type { mastodon } from 'masto'
defineProps<{ defineProps<{
card: Card card: mastodon.v1.PreviewCard
/** When it is root card in the list, not appear as a child card */ /** When it is root card in the list, not appear as a child card */
root?: boolean root?: boolean
/** For the preview image, only the small image mode is displayed */ /** For the preview image, only the small image mode is displayed */

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Card } from 'masto' import type { mastodon } from 'masto'
const props = defineProps<{ const props = defineProps<{
card: Card card: mastodon.v1.PreviewCard
}>() }>()
type UrlType = 'user' | 'repo' | 'issue' | 'pull' type UrlType = 'user' | 'repo' | 'issue' | 'pull'

View file

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const { status, collapsed = false, simplified = false } = defineProps<{ const {
status: Status status,
collapsed?: boolean isSelfReply = false,
simplified?: boolean } = defineProps<{
status: mastodon.v1.Status
isSelfReply: boolean
}>() }>()
const isSelf = $computed(() => status.inReplyToAccountId === status.account.id) const isSelf = $computed(() => status.inReplyToAccountId === status.account.id)
@ -12,21 +14,27 @@ const account = isSelf ? computed(() => status.account) : useAccountById(status.
</script> </script>
<template> <template>
<div v-if="status.inReplyToAccountId" flex="~ wrap" gap-1 items-end> <NuxtLink
<NuxtLink v-if="status.inReplyToId"
v-if="status.inReplyToId" flex="~ gap2" items-center h-auto text-sm text-secondary
flex="~" items-center h-auto font-bold text-sm text-secondary gap-1 :to="getStatusInReplyToRoute(status)"
:to="getStatusInReplyToRoute(status)" :title="$t('status.replying_to', [account ? getDisplayName(account) : $t('status.someone')])"
:title="account ? `Replying to ${getDisplayName(account)}` : 'Replying to someone'" text-blue saturate-50 hover:saturate-100
> >
<template v-if="account"> <template v-if="isSelfReply">
<div i-ri:reply-fill :class="collapsed ? '' : 'scale-x-[-1]'" text-secondary-light /> <div i-ri-discuss-line text-blue />
<template v-if="!collapsed"> <span>{{ $t('status.show_full_thread') }}</span>
<AccountAvatar v-if="isSelf || simplified || status.inReplyToAccountId === currentUser?.account.id" :account="account" :link="false" w-5 h-5 mx-0.5 /> </template>
<AccountInlineInfo v-else :account="account" :link="false" mx-0.5 /> <template v-else>
<div i-ri-chat-1-line text-blue />
<i18n-t keypath="status.replying_to">
<template v-if="account">
<AccountInlineInfo :account="account" :link="false" />
</template> </template>
</template> <template v-else>
<div i-ri:question-answer-line text-secondary-light text-lg /> {{ $t('status.someone') }}
</NuxtLink> </template>
</div> </i18n-t>
</template>
</NuxtLink>
</template> </template>

View file

@ -11,7 +11,7 @@ watchEffect(() => {
<template> <template>
<div v-if="enabled" flex flex-col items-start> <div v-if="enabled" flex flex-col items-start>
<div class="content-rich" px-4 pb-2.5 text-center text-secondary w-full border="~ base" border-0 border-b-dotted border-b-3 mt-2> <div class="content-rich" p="x-4 b-2.5" text-center text-secondary w-full border="~ base" border-0 border-b-dotted border-b-3 mt-2>
<slot name="spoiler" /> <slot name="spoiler" />
</div> </div>
<div flex="~ gap-1 center" w-full mt="-4.5"> <div flex="~ gap-1 center" w-full mt="-4.5">

View file

@ -1,43 +1,48 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status, StatusEdit } from 'masto' import type { mastodon } from 'masto'
import { formatTimeAgo } from '@vueuse/core' import { formatTimeAgo } from '@vueuse/core'
const { status } = defineProps<{ const { status } = defineProps<{
status: Status status: mastodon.v1.Status
}>() }>()
const masto = useMasto() const paginator = useMasto().v1.statuses.listHistory(status.id)
const { data: statusEdits } = useAsyncData(`status:history:${status.id}`, () => masto.statuses.fetchHistory(status.id).then(res => res.reverse()))
const showHistory = (edit: StatusEdit) => { const showHistory = (edit: mastodon.v1.StatusEdit) => {
openEditHistoryDialog(edit) openEditHistoryDialog(edit)
} }
const timeAgoOptions = useTimeAgoOptions() const timeAgoOptions = useTimeAgoOptions()
const reverseHistory = (items: mastodon.v1.StatusEdit[]) =>
[...items].reverse()
</script> </script>
<template> <template>
<template v-if="statusEdits"> <CommonPaginator :paginator="paginator" key-prop="createdAt" :preprocess="reverseHistory">
<CommonDropdownItem <template #default="{ items, item, index }">
v-for="(edit, idx) in statusEdits" <CommonDropdownItem
:key="idx" px="0.5"
px="0.5" @click="showHistory(item)"
@click="showHistory(edit)" >
> {{ getDisplayName(item.account) }}
{{ getDisplayName(edit.account) }}
<template v-if="idx === statusEdits.length - 1"> <template v-if="index === items.length - 1">
<i18n-t keypath="status_history.created"> <i18n-t keypath="status_history.created">
{{ formatTimeAgo(new Date(edit.createdAt), timeAgoOptions) }} {{ formatTimeAgo(new Date(item.createdAt), timeAgoOptions) }}
</i18n-t>
</template>
<i18n-t v-else keypath="status_history.edited">
{{ formatTimeAgo(new Date(item.createdAt), timeAgoOptions) }}
</i18n-t> </i18n-t>
</template> </CommonDropdownItem>
<template v-else> </template>
<i18n-t keypath="status_history.edited"> <template #loading>
{{ formatTimeAgo(new Date(edit.createdAt), timeAgoOptions) }} <StatusEditHistorySkeleton />
</i18n-t> <StatusEditHistorySkeleton op50 />
</template> <StatusEditHistorySkeleton op25 />
</CommonDropdownItem> </template>
</template> <template #done>
<template v-else> <span />
<div i-ri:loader-2-fill animate-spin text-2xl ma /> </template>
</template> </CommonPaginator>
</template> </template>

View file

@ -0,0 +1,3 @@
<template>
<div class="skeleton-loading-bg" h-5 w-full rounded my2 />
</template>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { mastodon } from 'masto'
const { status } = defineProps<{ const { status } = defineProps<{
status: Status status: mastodon.v1.Status
inline: boolean inline: boolean
}>() }>()

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { StatusEdit } from 'masto' import type { mastodon } from 'masto'
const { edit } = defineProps<{ const { edit } = defineProps<{
edit: StatusEdit edit: mastodon.v1.StatusEdit
}>() }>()
</script> </script>

View file

@ -1,21 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Tag } from 'masto' import type { mastodon } from 'masto'
const { tag } = defineProps<{ const { tag } = defineProps<{
tag: Tag tag: mastodon.v1.Tag
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'change'): void (event: 'change'): void
}>() }>()
const { tags } = useMasto() const masto = useMasto()
const toggleFollowTag = async () => { const toggleFollowTag = async () => {
if (tag.following) if (tag.following)
await tags.unfollow(tag.name) await masto.v1.tags.unfollow(tag.name)
else else
await tags.follow(tag.name) await masto.v1.tags.follow(tag.name)
emit('change') emit('change')
} }

View file

@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Tag } from 'masto' import type { mastodon } from 'masto'
const { const {
tag, tag,
} = $defineProps<{ } = $defineProps<{
tag: Tag tag: mastodon.v1.Tag
}>() }>()
const to = $computed(() => new URL(tag.url).pathname) const to = $computed(() => new URL(tag.url).pathname)

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { Paginator, mastodon } from 'masto'
const { paginator } = defineProps<{
paginator: Paginator<mastodon.v1.Tag[], mastodon.DefaultPaginationParams>
}>()
</script>
<template>
<CommonPaginator :paginator="paginator" key-prop="name">
<template #default="{ item }">
<TagCard :tag="item" border="b base" />
</template>
<template #loading>
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op25 />
</template>
</CommonPaginator>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().blocks.iterate() const paginator = useMasto().v1.blocks.list()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().bookmarks.iterate() const paginator = useMasto().v1.bookmarks.list()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().conversations.iterate() const paginator = useMasto().v1.conversations.list()
</script> </script>
<template> <template>

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const masto = useMasto() const masto = useMasto()
const paginator = masto.domainBlocks.iterate() const paginator = masto.v1.domainBlocks.list()
const unblock = async (domain: string) => { const unblock = async (domain: string) => {
await masto.domainBlocks.unblock(domain) await masto.v1.domainBlocks.unblock(domain)
} }
</script> </script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().favourites.iterate() const paginator = useMasto().v1.favourites.list()
</script> </script>
<template> <template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().timelines.iterateHome() const paginator = useMasto().v1.timelines.listHome()
const stream = useMasto().stream.streamUser() const stream = useMasto().v1.stream.streamUser()
onBeforeUnmount(() => stream?.then(s => s.disconnect())) onBeforeUnmount(() => stream?.then(s => s.disconnect()))
</script> </script>

View file

@ -1,13 +0,0 @@
<script setup lang="ts">
import type { Status } from 'masto'
defineProps<{
timelines: Status[]
}>()
</script>
<template>
<template v-for="status of timelines" :key="status.id">
<StatusCard :status="status" border="t base" />
</template>
</template>

View file

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
// Default limit is 20 notifications, and servers are normally caped to 30 // Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMasto().notifications.iterate({ limit: 30, types: ['mention'] }) const paginator = useMasto().v1.notifications.list({ limit: 30, types: ['mention'] })
const { clearNotifications } = useNotifications() const { clearNotifications } = useNotifications()
onActivated(clearNotifications) onActivated(clearNotifications)
const stream = useMasto().stream.streamUser() const stream = useMasto().v1.stream.streamUser()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().mutes.iterate() const paginator = useMasto().v1.mutes.list()
</script> </script>
<template> <template>

View file

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
// Default limit is 20 notifications, and servers are normally caped to 30 // Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMasto().notifications.iterate({ limit: 30 }) const paginator = useMasto().v1.notifications.list({ limit: 30 })
const { clearNotifications } = useNotifications() const { clearNotifications } = useNotifications()
onActivated(clearNotifications) onActivated(clearNotifications)
const stream = useMasto().stream.streamUser() const stream = useMasto().v1.stream.streamUser()
</script> </script>
<template> <template>

View file

@ -2,14 +2,14 @@
// @ts-expect-error missing types // @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller' import { DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import type { Account, FilterContext, Paginator, Status, WsEvents } from 'masto' import type { Paginator, WsEvents, mastodon } from 'masto'
const { paginator, stream, account } = defineProps<{ const { paginator, stream, account } = defineProps<{
paginator: Paginator<any, Status[]> paginator: Paginator<mastodon.v1.Status[], mastodon.v1.ListAccountStatusesParams>
stream?: Promise<WsEvents> stream?: Promise<WsEvents>
context?: FilterContext context?: mastodon.v2.FilterContext
account?: Account account?: mastodon.v1.Account
preprocess?: (items: any[]) => any[] preprocess?: (items: mastodon.v1.Status[]) => mastodon.v1.Status[]
}>() }>()
const { formatNumber } = useHumanReadableNumber() const { formatNumber } = useHumanReadableNumber()

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().accounts.iterateStatuses(currentUser.value!.account.id, { pinned: true }) const paginator = useMasto().v1.accounts.listStatuses(currentUser.value!.account.id, { pinned: true })
</script> </script>
<template> <template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().timelines.iteratePublic() const paginator = useMasto().v1.timelines.listPublic()
const stream = useMasto().stream.streamPublicTimeline() const stream = useMasto().v1.stream.streamPublicTimeline()
onBeforeUnmount(() => stream.then(s => s.disconnect())) onBeforeUnmount(() => stream.then(s => s.disconnect()))
</script> </script>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().timelines.iteratePublic({ local: true }) const paginator = useMasto().v1.timelines.listPublic({ local: true })
const stream = useMasto().stream.streamCommunityTimeline() const stream = useMasto().v1.stream.streamCommunityTimeline()
onBeforeUnmount(() => stream.then(s => s.disconnect())) onBeforeUnmount(() => stream.then(s => s.disconnect()))
</script> </script>

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Tag } from 'masto' import type { mastodon } from 'masto'
import CommonScrollIntoView from '../common/CommonScrollIntoView.vue' import CommonScrollIntoView from '../common/CommonScrollIntoView.vue'
const { items, command } = defineProps<{ const { items, command } = defineProps<{
items: Tag[] items: mastodon.v1.Tag[]
command: Function command: Function
isPending?: boolean isPending?: boolean
}>() }>()

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { mastodon } from 'masto'
import CommonScrollIntoView from '../common/CommonScrollIntoView.vue' import CommonScrollIntoView from '../common/CommonScrollIntoView.vue'
const { items, command } = defineProps<{ const { items, command } = defineProps<{
items: Account[] items: mastodon.v1.Account[]
command: Function command: Function
isPending?: boolean isPending?: boolean
}>() }>()

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { $fetch } from 'ofetch' import { $fetch } from 'ofetch'
import { DEFAULT_SERVER } from '~/constants'
const input = $ref<HTMLInputElement>() const input = $ref<HTMLInputElement>()
let server = $ref<string>('') let server = $ref<string>('')
@ -26,7 +25,7 @@ async function oauth() {
server = server.split('/')[0] server = server.split('/')[0]
try { try {
location.href = await $fetch<string>(`/api/${server || DEFAULT_SERVER}/login`, { location.href = await $fetch<string>(`/api/${server || publicServer.value}/login`, {
method: 'POST', method: 'POST',
body: { body: {
origin: location.origin, origin: location.origin,

View file

@ -1,3 +1,5 @@
import type { BuildInfo } from '~~/types'
export interface Team { export interface Team {
github: string github: string
display: string display: string
@ -31,3 +33,7 @@ export const teams: Team[] = [
mastodon: 'sxzz@webtoo.ls', mastodon: 'sxzz@webtoo.ls',
}, },
].sort(() => Math.random() - 0.5) ].sort(() => Math.random() - 0.5)
export function useBuildInfo() {
return useRuntimeConfig().public.buildInfo as BuildInfo
}

View file

@ -1,5 +1,5 @@
import LRU from 'lru-cache' import LRU from 'lru-cache'
import type { Account, Status } from 'masto' import type { mastodon } from 'masto'
const cache = new LRU<string, any>({ const cache = new LRU<string, any>({
max: 1000, max: 1000,
@ -17,13 +17,13 @@ function removeCached(key: string) {
cache.delete(key) cache.delete(key)
} }
export function fetchStatus(id: string, force = false): Promise<Status> { export function fetchStatus(id: string, force = false): Promise<mastodon.v1.Status> {
const server = currentServer.value const server = currentServer.value
const key = `${server}:status:${id}` 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
const promise = useMasto().statuses.fetch(id) const promise = useMasto().v1.statuses.fetch(id)
.then((status) => { .then((status) => {
cacheStatus(status) cacheStatus(status)
return status return status
@ -32,7 +32,7 @@ export function fetchStatus(id: string, force = false): Promise<Status> {
return promise return promise
} }
export function fetchAccountById(id?: string | null): Promise<Account | null> { export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Account | null> {
if (!id) if (!id)
return Promise.resolve(null) return Promise.resolve(null)
@ -41,11 +41,11 @@ export function fetchAccountById(id?: string | null): Promise<Account | null> {
const cached = cache.get(key) const cached = cache.get(key)
if (cached) if (cached)
return cached return cached
const uri = currentInstance.value?.uri const domain = currentInstance.value?.uri
const promise = useMasto().accounts.fetch(id) const promise = useMasto().v1.accounts.fetch(id)
.then((r) => { .then((r) => {
if (r.acct && !r.acct.includes('@') && uri) if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${uri}` r.acct = `${r.acct}@${domain}`
cacheAccount(r, server, true) cacheAccount(r, server, true)
return r return r
@ -54,17 +54,17 @@ export function fetchAccountById(id?: string | null): Promise<Account | null> {
return promise return promise
} }
export async function fetchAccountByHandle(acct: string): Promise<Account> { export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> {
const server = currentServer.value const server = currentServer.value
const key = `${server}:account:${acct}` 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 domain = currentInstance.value?.uri
const account = useMasto().accounts.lookup({ acct }) const account = useMasto().v1.accounts.lookup({ acct })
.then((r) => { .then((r) => {
if (r.acct && !r.acct.includes('@') && uri) if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${uri}` r.acct = `${r.acct}@${domain}`
cacheAccount(r, server, true) cacheAccount(r, server, true)
return r return r
@ -81,7 +81,7 @@ export function useAccountById(id?: string | null) {
return useAsyncState(() => fetchAccountById(id), null).state return useAsyncState(() => fetchAccountById(id), null).state
} }
export function cacheStatus(status: Status, server = currentServer.value, override?: boolean) { export function cacheStatus(status: mastodon.v1.Status, server = currentServer.value, override?: boolean) {
setCached(`${server}:status:${status.id}`, status, override) setCached(`${server}:status:${status.id}`, status, override)
} }
@ -89,7 +89,7 @@ export function removeCachedStatus(id: string, server = currentServer.value) {
removeCached(`${server}:status:${id}`) removeCached(`${server}:status:${id}`)
} }
export function cacheAccount(account: Account, server = currentServer.value, override?: boolean) { export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) {
setCached(`${server}:account:${account.id}`, account, override) setCached(`${server}:account:${account.id}`, account, override)
setCached(`${server}:account:${account.acct}`, account, override) setCached(`${server}:account:${account.acct}`, account, override)
} }

View file

@ -2,7 +2,7 @@ import type { ComputedRef } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import type { LocaleObject } from '#i18n' import type { LocaleObject } from '#i18n'
import type { SearchResult } from '@/components/search/types' import type { SearchResult } from '~/composables/masto/search'
// @unocss-include // @unocss-include

View file

@ -1,15 +1,16 @@
// @unimport-disable // @unimport-disable
import type { Emoji } from 'masto' import type { mastodon } from 'masto'
import type { Node } from 'ultrahtml' import type { Node } from 'ultrahtml'
import { ELEMENT_NODE, TEXT_NODE, h, parse, render } from 'ultrahtml' import { DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE, h, parse, render } from 'ultrahtml'
import { findAndReplaceEmojisInText } from '@iconify/utils' import { findAndReplaceEmojisInText } from '@iconify/utils'
import { emojiRegEx, getEmojiAttributes } from '../config/emojis' import { emojiRegEx, getEmojiAttributes } from '../config/emojis'
export interface ContentParseOptions { export interface ContentParseOptions {
emojis?: Record<string, Emoji> emojis?: Record<string, mastodon.v1.CustomEmoji>
markdown?: boolean markdown?: boolean
replaceUnicodeEmoji?: boolean replaceUnicodeEmoji?: boolean
astTransforms?: Transform[] astTransforms?: Transform[]
convertMentionLink?: boolean
} }
const sanitizerBasicClasses = filterClasses(/^(h-\S*|p-\S*|u-\S*|dt-\S*|e-\S*|mention|hashtag|ellipsis|invisible)$/u) const sanitizerBasicClasses = filterClasses(/^(h-\S*|p-\S*|u-\S*|dt-\S*|e-\S*|mention|hashtag|ellipsis|invisible)$/u)
@ -53,6 +54,7 @@ export function parseMastodonHTML(
const { const {
markdown = true, markdown = true,
replaceUnicodeEmoji = true, replaceUnicodeEmoji = true,
convertMentionLink = false,
} = options } = options
if (markdown) { if (markdown) {
@ -77,19 +79,25 @@ export function parseMastodonHTML(
if (markdown) if (markdown)
transforms.push(transformMarkdown) transforms.push(transformMarkdown)
if (convertMentionLink)
transforms.push(transformMentionLink)
transforms.push(replaceCustomEmoji(options.emojis || {})) transforms.push(replaceCustomEmoji(options.emojis || {}))
transforms.push(transformParagraphs)
return transformSync(parse(html), transforms) return transformSync(parse(html), transforms)
} }
/** /**
* Converts raw HTML form Mastodon server to HTML for Tiptap editor * Converts raw HTML form Mastodon server to HTML for Tiptap editor
*/ */
export function convertMastodonHTML(html: string, customEmojis: Record<string, Emoji> = {}) { export function convertMastodonHTML(html: string, customEmojis: Record<string, mastodon.v1.CustomEmoji> = {}) {
const tree = parseMastodonHTML(html, { const tree = parseMastodonHTML(html, {
emojis: customEmojis, emojis: customEmojis,
markdown: true, markdown: true,
replaceUnicodeEmoji: false, replaceUnicodeEmoji: false,
convertMentionLink: true,
}) })
return render(tree) return render(tree)
} }
@ -285,7 +293,7 @@ function transformUnicodeEmoji(node: Node) {
return matches.filter(Boolean) return matches.filter(Boolean)
} }
function replaceCustomEmoji(customEmojis: Record<string, Emoji>): Transform { function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => { return (node) => {
if (node.type !== TEXT_NODE) if (node.type !== TEXT_NODE)
return node return node
@ -313,6 +321,8 @@ const _markdownReplacements: [RegExp, (c: (string | Node)[]) => Node][] = [
[/\*(.*?)\*/g, c => h('em', null, c)], [/\*(.*?)\*/g, c => h('em', null, c)],
[/~~(.*?)~~/g, c => h('del', null, c)], [/~~(.*?)~~/g, c => h('del', null, c)],
[/`([^`]+?)`/g, c => h('code', null, c)], [/`([^`]+?)`/g, c => h('code', null, c)],
// transform @username@twitter.com as links
[/\B@([a-zA-Z0-9_]+)@twitter\.com\b/gi, c => h('a', { href: `https://twitter.com/${c}`, target: '_blank', class: 'mention external' }, `@${c}@twitter.com`)],
] ]
function _markdownProcess(value: string) { function _markdownProcess(value: string) {
@ -349,3 +359,26 @@ function transformMarkdown(node: Node) {
return node return node
return _markdownProcess(node.value) return _markdownProcess(node.value)
} }
function transformParagraphs(node: Node): Node | Node[] {
// For top level paragraphs, inject an empty <p> to preserve status paragraphs in our editor (except for the last one)
if (node.parent?.type === DOCUMENT_NODE && node.name === 'p' && node.parent.children.at(-1) !== node)
return [node, h('p')]
return node
}
function transformMentionLink(node: Node): string | Node | (string | Node)[] | null {
if (node.name === 'a' && node.attributes.class?.includes('mention')) {
const href = node.attributes.href
if (href) {
const matchUser = href.match(UserLinkRE)
if (matchUser) {
const [, server, username] = matchUser
const handle = `${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
// convert to TipTap mention node
return h('span', { 'data-type': 'mention', 'data-id': handle }, handle)
}
}
}
return node
}

View file

@ -1,14 +1,14 @@
import type { Attachment, Status, StatusEdit } from 'masto' import type { mastodon } from 'masto'
import type { ConfirmDialogChoice, ConfirmDialogLabel, Draft } from '~/types' import type { ConfirmDialogChoice, ConfirmDialogLabel, Draft } from '~/types'
import { STORAGE_KEY_FIRST_VISIT } from '~/constants' import { STORAGE_KEY_FIRST_VISIT } from '~/constants'
export const confirmDialogChoice = ref<ConfirmDialogChoice>() export const confirmDialogChoice = ref<ConfirmDialogChoice>()
export const confirmDialogLabel = ref<ConfirmDialogLabel>() export const confirmDialogLabel = ref<ConfirmDialogLabel>()
export const mediaPreviewList = ref<Attachment[]>([]) export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
export const mediaPreviewIndex = ref(0) export const mediaPreviewIndex = ref(0)
export const statusEdit = ref<StatusEdit>() export const statusEdit = ref<mastodon.v1.StatusEdit>()
export const dialogDraftKey = ref<string>() export const dialogDraftKey = ref<string>()
export const commandPanelInput = ref('') export const commandPanelInput = ref('')
@ -23,7 +23,7 @@ export const isPreviewHelpOpen = ref(isFirstVisit.value)
export const isCommandPanelOpen = ref(false) export const isCommandPanelOpen = ref(false)
export const isConfirmDialogOpen = ref(false) export const isConfirmDialogOpen = ref(false)
export const lastPublishDialogStatus = ref<Status | null>(null) export const lastPublishDialogStatus = ref<mastodon.v1.Status | null>(null)
export function openSigninDialog() { export function openSigninDialog() {
isSigninDialogOpen.value = true isSigninDialogOpen.value = true
@ -80,7 +80,7 @@ if (process.client) {
restoreMediaPreviewFromState() restoreMediaPreviewFromState()
} }
export function openMediaPreview(attachments: Attachment[], index = 0) { export function openMediaPreview(attachments: mastodon.v1.MediaAttachment[], index = 0) {
mediaPreviewList.value = attachments mediaPreviewList.value = attachments
mediaPreviewIndex.value = index mediaPreviewIndex.value = index
isMediaPreviewOpen.value = true isMediaPreviewOpen.value = true
@ -97,7 +97,7 @@ export function closeMediaPreview() {
history.back() history.back()
} }
export function openEditHistoryDialog(edit: StatusEdit) { export function openEditHistoryDialog(edit: mastodon.v1.StatusEdit) {
statusEdit.value = edit statusEdit.value = edit
isEditHistoryDialogOpen.value = true isEditHistoryDialogOpen.value = true
} }

View file

@ -1,4 +1,4 @@
import type { Emoji } from 'masto' import type { mastodon } from 'masto'
import type { CustomEmojisInfo } from './push-notifications/types' import type { CustomEmojisInfo } from './push-notifications/types'
import { STORAGE_KEY_CUSTOM_EMOJIS } from '~/constants' import { STORAGE_KEY_CUSTOM_EMOJIS } from '~/constants'
@ -20,14 +20,14 @@ export async function updateCustomEmojis() {
return return
const masto = useMasto() const masto = useMasto()
const emojis = await masto.customEmojis.fetchAll() const emojis = await masto.v1.customEmojis.list()
Object.assign(currentCustomEmojis.value, { Object.assign(currentCustomEmojis.value, {
lastUpdate: Date.now(), lastUpdate: Date.now(),
emojis, emojis,
}) })
} }
function transformEmojiData(emojis: Emoji[]) { function transformEmojiData(emojis: mastodon.v1.CustomEmoji[]) {
const result = [] const result = []
for (const emoji of emojis) { for (const emoji of emojis) {
@ -52,9 +52,9 @@ export const customEmojisData = computed(() => currentCustomEmojis.value.emojis.
}] }]
: undefined) : undefined)
export function useEmojisFallback(emojisGetter: () => Emoji[] | undefined) { export function useEmojisFallback(emojisGetter: () => mastodon.v1.CustomEmoji[] | undefined) {
return computed(() => { return computed(() => {
const result: Emoji[] = [] const result: mastodon.v1.CustomEmoji[] = []
const emojis = emojisGetter() const emojis = emojisGetter()
if (emojis) if (emojis)
result.push(...emojis) result.push(...emojis)

View file

@ -2,11 +2,11 @@ import type { MaybeComputedRef, MaybeRef, UseTimeAgoOptions } from '@vueuse/core
const formatter = Intl.NumberFormat() const formatter = Intl.NumberFormat()
export const formattedNumber = (num: number, useFormatter: Intl.NumberFormat = formatter) => { export function formattedNumber(num: number, useFormatter: Intl.NumberFormat = formatter) {
return useFormatter.format(num) return useFormatter.format(num)
} }
export const useHumanReadableNumber = () => { export function useHumanReadableNumber() {
const { n, locale } = useI18n() const { n, locale } = useI18n()
const fn = (num: number) => { const fn = (num: number) => {
@ -29,10 +29,8 @@ export const useHumanReadableNumber = () => {
} }
} }
export const useFormattedDateTime = ( export function useFormattedDateTime(value: MaybeComputedRef<string | number | Date | undefined | null>,
value: MaybeComputedRef<string | number | Date | undefined | null>, options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' }) {
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' },
) => {
const { locale } = useI18n() const { locale } = useI18n()
const formatter = $computed(() => Intl.DateTimeFormat(locale.value, options)) const formatter = $computed(() => Intl.DateTimeFormat(locale.value, options))
return computed(() => { return computed(() => {
@ -41,7 +39,7 @@ export const useFormattedDateTime = (
}) })
} }
export const useTimeAgoOptions = (short = false): UseTimeAgoOptions<false> => { export function useTimeAgoOptions(short = false): UseTimeAgoOptions<false> {
const { d, t, n: fnf, locale } = useI18n() const { d, t, n: fnf, locale } = useI18n()
const prefix = short ? 'short_' : '' const prefix = short ? 'short_' : ''
@ -56,7 +54,7 @@ export const useTimeAgoOptions = (short = false): UseTimeAgoOptions<false> => {
return { return {
rounding: 'floor', rounding: 'floor',
showSecond: !short, showSecond: !short,
updateInterval: short ? 60_000 : 1_000, updateInterval: short ? 60000 : 1000,
messages: { messages: {
justNow: t('time_ago_options.just_now'), justNow: t('time_ago_options.just_now'),
// just return the value // just return the value

View file

@ -1,26 +1,26 @@
import type { Account } from 'masto' import type { mastodon } from 'masto'
export function getDisplayName(account?: Account, options?: { rich?: boolean }) { export function getDisplayName(account?: mastodon.v1.Account, options?: { rich?: boolean }) {
const displayName = account?.displayName || account?.username || '' const displayName = account?.displayName || account?.username || ''
if (options?.rich) if (options?.rich)
return displayName return displayName
return displayName.replace(/:([\w-]+?):/g, '') return displayName.replace(/:([\w-]+?):/g, '')
} }
export function getShortHandle({ acct }: Account) { export function getShortHandle({ acct }: mastodon.v1.Account) {
if (!acct) if (!acct)
return '' return ''
return `@${acct.includes('@') ? acct.split('@')[0] : acct}` return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
} }
export function getServerName(account: Account) { export function getServerName(account: mastodon.v1.Account) {
if (account.acct?.includes('@')) if (account.acct?.includes('@'))
return account.acct.split('@')[1] return account.acct.split('@')[1]
// We should only lack the server name if we're on the same server as the account // We should only lack the server name if we're on the same server as the account
return currentInstance.value?.uri || '' return currentInstance.value?.uri || ''
} }
export function getFullHandle(account: Account) { export function getFullHandle(account: mastodon.v1.Account) {
const handle = `@${account.acct}` const handle = `@${account.acct}`
if (!currentUser.value || account.acct.includes('@')) if (!currentUser.value || account.acct.includes('@'))
return handle return handle
@ -36,7 +36,7 @@ export function toShortHandle(fullHandle: string) {
return fullHandle return fullHandle
} }
export function extractAccountHandle(account: Account) { export function extractAccountHandle(account: mastodon.v1.Account) {
let handle = getFullHandle(account).slice(1) let handle = getFullHandle(account).slice(1)
const uri = currentInstance.value?.uri ?? currentServer.value const uri = currentInstance.value?.uri ?? currentServer.value
if (currentInstance.value && handle.endsWith(`@${uri}`)) if (currentInstance.value && handle.endsWith(`@${uri}`))
@ -45,7 +45,7 @@ export function extractAccountHandle(account: Account) {
return handle return handle
} }
export function useAccountHandle(account: Account, fullServer = true) { export function useAccountHandle(account: mastodon.v1.Account, fullServer = true) {
return computed(() => fullServer return computed(() => fullServer
? getFullHandle(account) ? getFullHandle(account)
: getShortHandle(account), : getShortHandle(account),

View file

@ -1,20 +1,20 @@
import type { Account, Relationship } from 'masto' import type { mastodon } from 'masto'
import type { Ref } from 'vue' import type { Ref } from 'vue'
// Batch requests for relationships when used in the UI // Batch requests for relationships when used in the UI
// We don't want to hold to old values, so every time a Relationship is needed it // We don't want to hold to old values, so every time a Relationship is needed it
// is requested again from the server to show the latest state // is requested again from the server to show the latest state
const requestedRelationships = new Map<string, Ref<Relationship | undefined>>() const requestedRelationships = new Map<string, Ref<mastodon.v1.Relationship | undefined>>()
let timeoutHandle: NodeJS.Timeout | undefined let timeoutHandle: NodeJS.Timeout | undefined
export function useRelationship(account: Account): Ref<Relationship | undefined> { export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.Relationship | undefined> {
if (!currentUser.value) if (!currentUser.value)
return ref() return ref()
let relationship = requestedRelationships.get(account.id) let relationship = requestedRelationships.get(account.id)
if (relationship) if (relationship)
return relationship return relationship
relationship = ref<Relationship | undefined>() relationship = ref<mastodon.v1.Relationship | undefined>()
requestedRelationships.set(account.id, relationship) requestedRelationships.set(account.id, relationship)
if (timeoutHandle) if (timeoutHandle)
clearTimeout(timeoutHandle) clearTimeout(timeoutHandle)
@ -27,7 +27,7 @@ export function useRelationship(account: Account): Ref<Relationship | undefined>
async function fetchRelationships() { async function fetchRelationships() {
const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value) const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value)
const relationships = await useMasto().accounts.fetchRelationships(requested.map(([id]) => id)) const relationships = await useMasto().v1.accounts.fetchRelationships(requested.map(([id]) => id))
for (let i = 0; i < requested.length; i++) for (let i = 0; i < requested.length; i++)
requested[i][1].value = relationships[i] requested[i][1].value = relationships[i]
} }

View file

@ -1,7 +1,7 @@
import { withoutProtocol } from 'ufo' import { withoutProtocol } from 'ufo'
import type { Account, Status } from 'masto' import type { mastodon } from 'masto'
export function getAccountRoute(account: Account) { export function getAccountRoute(account: mastodon.v1.Account) {
return useRouter().resolve({ return useRouter().resolve({
name: 'account-index', name: 'account-index',
params: { params: {
@ -10,7 +10,7 @@ export function getAccountRoute(account: Account) {
}, },
}) })
} }
export function getAccountFollowingRoute(account: Account) { export function getAccountFollowingRoute(account: mastodon.v1.Account) {
return useRouter().resolve({ return useRouter().resolve({
name: 'account-following', name: 'account-following',
params: { params: {
@ -19,7 +19,7 @@ export function getAccountFollowingRoute(account: Account) {
}, },
}) })
} }
export function getAccountFollowersRoute(account: Account) { export function getAccountFollowersRoute(account: mastodon.v1.Account) {
return useRouter().resolve({ return useRouter().resolve({
name: 'account-followers', name: 'account-followers',
params: { params: {
@ -29,7 +29,7 @@ export function getAccountFollowersRoute(account: Account) {
}) })
} }
export function getStatusRoute(status: Status) { export function getStatusRoute(status: mastodon.v1.Status) {
return useRouter().resolve({ return useRouter().resolve({
name: 'status', name: 'status',
params: { params: {
@ -50,11 +50,11 @@ export function getTagRoute(tag: string) {
}) })
} }
export function getStatusPermalinkRoute(status: Status) { export function getStatusPermalinkRoute(status: mastodon.v1.Status) {
return status.url ? withoutProtocol(status.url) : null return status.url ? withoutProtocol(status.url) : null
} }
export function getStatusInReplyToRoute(status: Status) { export function getStatusInReplyToRoute(status: mastodon.v1.Status) {
return useRouter().resolve({ return useRouter().resolve({
name: 'status-by-id', name: 'status-by-id',
params: { params: {

View file

@ -1,19 +1,64 @@
import type { MaybeRef } from '@vueuse/core' import type { MaybeComputedRef } from '@vueuse/core'
import type { Account, Paginator, Results, SearchParams, Status } from 'masto' import type { Paginator, mastodon } from 'masto'
import type { RouteLocation } from 'vue-router'
export interface UseSearchOptions { export type UseSearchOptions = MaybeComputedRef<
type?: MaybeRef<'accounts' | 'hashtags' | 'statuses'> Partial<Omit<mastodon.v1.SearchParams, keyof mastodon.DefaultPaginationParams | 'q'>>
>
export interface BuildSearchResult<K extends keyof any, T> {
id: string
type: K
data: T
to: RouteLocation & {
href: string
}
} }
export type AccountSearchResult = BuildSearchResult<'account', mastodon.v1.Account>
export type HashTagSearchResult = BuildSearchResult<'hashtag', mastodon.v1.Tag>
export type StatusSearchResult = BuildSearchResult<'status', mastodon.v1.Status>
export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) { export type SearchResult = HashTagSearchResult | AccountSearchResult | StatusSearchResult
export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOptions = {}) {
const done = ref(false) const done = ref(false)
const masto = useMasto() const masto = useMasto()
const loading = ref(false) const loading = ref(false)
const statuses = ref<Status[]>([]) const accounts = ref<AccountSearchResult[]>([])
const accounts = ref<Account[]>([]) const hashtags = ref<HashTagSearchResult[]>([])
const hashtags = ref<any[]>([]) const statuses = ref<StatusSearchResult[]>([])
let paginator: Paginator<SearchParams, Results> | undefined let paginator: Paginator<mastodon.v2.Search, mastodon.v2.SearchParams> | undefined
const appendResults = (results: mastodon.v2.Search, empty = false) => {
if (empty) {
accounts.value = []
hashtags.value = []
statuses.value = []
}
accounts.value = [...accounts.value, ...results.accounts.map<AccountSearchResult>(account => ({
type: 'account',
id: account.id,
data: account,
to: getAccountRoute(account),
}))]
hashtags.value = [...hashtags.value, ...results.hashtags.map<HashTagSearchResult>(hashtag => ({
type: 'hashtag',
id: `hashtag-${hashtag.name}`,
data: hashtag,
to: getTagRoute(hashtag.name),
}))]
statuses.value = [...statuses.value, ...results.statuses.map<StatusSearchResult>(status => ({
type: 'status',
id: status.id,
data: status,
to: getStatusRoute(status),
}))]
}
watch(() => unref(query), () => {
loading.value = !!(unref(query) && isMastoInitialised.value)
})
debouncedWatch(() => unref(query), async () => { debouncedWatch(() => unref(query), async () => {
if (!unref(query) || !isMastoInitialised.value) if (!unref(query) || !isMastoInitialised.value)
@ -25,17 +70,19 @@ export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) {
* Based on the source it seems like modifying the params when calling next would result in a new search, * Based on the source it seems like modifying the params when calling next would result in a new search,
* but that doesn't seem to be the case. So instead we just create a new paginator with the new params. * but that doesn't seem to be the case. So instead we just create a new paginator with the new params.
*/ */
paginator = masto.search({ q: unref(query), resolve: !!currentUser.value, type: unref(options?.type) }) paginator = masto.v2.search({
q: resolveUnref(query),
...resolveUnref(options),
resolve: !!currentUser.value,
})
const nextResults = await paginator.next() const nextResults = await paginator.next()
done.value = nextResults.done || false done.value = !!nextResults.done
if (!nextResults.done)
statuses.value = nextResults.value?.statuses || [] appendResults(nextResults.value, true)
accounts.value = nextResults.value?.accounts || []
hashtags.value = nextResults.value?.hashtags || []
loading.value = false loading.value = false
}, { debounce: 500 }) }, { debounce: 300 })
const next = async () => { const next = async () => {
if (!unref(query) || !isMastoInitialised.value || !paginator) if (!unref(query) || !isMastoInitialised.value || !paginator)
@ -45,19 +92,9 @@ export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) {
const nextResults = await paginator.next() const nextResults = await paginator.next()
loading.value = false loading.value = false
done.value = nextResults.done || false done.value = !!nextResults.done
statuses.value = [ if (!nextResults.done)
...statuses.value, appendResults(nextResults.value)
...(nextResults.value.statuses || []),
]
accounts.value = [
...statuses.value,
...(nextResults.value.accounts || []),
]
hashtags.value = [
...statuses.value,
...(nextResults.value.statuses || []),
]
} }
return { return {

View file

@ -1,14 +1,14 @@
import type { Status } from 'masto' import type { mastodon } from 'masto'
type Action = 'reblogged' | 'favourited' | 'bookmarked' | 'pinned' | 'muted' type Action = 'reblogged' | 'favourited' | 'bookmarked' | 'pinned' | 'muted'
type CountField = 'reblogsCount' | 'favouritesCount' type CountField = 'reblogsCount' | 'favouritesCount'
export interface StatusActionsProps { export interface StatusActionsProps {
status: Status status: mastodon.v1.Status
} }
export function useStatusActions(props: StatusActionsProps) { export function useStatusActions(props: StatusActionsProps) {
let status = $ref<Status>({ ...props.status }) let status = $ref<mastodon.v1.Status>({ ...props.status })
const masto = useMasto() const masto = useMasto()
watch( watch(
@ -27,10 +27,11 @@ export function useStatusActions(props: StatusActionsProps) {
muted: false, muted: false,
}) })
async function toggleStatusAction(action: Action, fetchNewStatus: () => Promise<Status>, countField?: CountField) { async function toggleStatusAction(action: Action, fetchNewStatus: () => Promise<mastodon.v1.Status>, countField?: CountField) {
// check login // check login
if (!checkLogin()) if (!checkLogin())
return return
isLoading[action] = true isLoading[action] = true
fetchNewStatus().then((newStatus) => { fetchNewStatus().then((newStatus) => {
Object.assign(status, newStatus) Object.assign(status, newStatus)
@ -44,9 +45,15 @@ export function useStatusActions(props: StatusActionsProps) {
if (countField) if (countField)
status[countField] += status[action] ? 1 : -1 status[countField] += status[action] ? 1 : -1
} }
const canReblog = $computed(() =>
status.visibility !== 'direct'
&& (status.visibility !== 'private' || status.account.id === currentUser.value?.account.id),
)
const toggleReblog = () => toggleStatusAction( const toggleReblog = () => toggleStatusAction(
'reblogged', 'reblogged',
() => masto.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => { () => masto.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => {
if (status.reblogged) if (status.reblogged)
// returns the original status // returns the original status
return res.reblog! return res.reblog!
@ -57,28 +64,29 @@ export function useStatusActions(props: StatusActionsProps) {
const toggleFavourite = () => toggleStatusAction( const toggleFavourite = () => toggleStatusAction(
'favourited', 'favourited',
() => masto.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id), () => masto.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
'favouritesCount', 'favouritesCount',
) )
const toggleBookmark = () => toggleStatusAction( const toggleBookmark = () => toggleStatusAction(
'bookmarked', 'bookmarked',
() => masto.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id), () => masto.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
) )
const togglePin = async () => toggleStatusAction( const togglePin = async () => toggleStatusAction(
'pinned', 'pinned',
() => masto.statuses[status.pinned ? 'unpin' : 'pin'](status.id), () => masto.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
) )
const toggleMute = async () => toggleStatusAction( const toggleMute = async () => toggleStatusAction(
'muted', 'muted',
() => masto.statuses[status.muted ? 'unmute' : 'mute'](status.id), () => masto.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id),
) )
return { return {
status: $$(status), status: $$(status),
isLoading: $$(isLoading), isLoading: $$(isLoading),
canReblog: $$(canReblog),
toggleMute, toggleMute,
toggleReblog, toggleReblog,
toggleFavourite, toggleFavourite,

Some files were not shown because too many files have changed in this diff Show more