1
0
mirror of https://github.com/elk-zone/elk synced 2024-11-23 14:46:08 +09:00

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_DEFAULT_SERVER=
# Production only
NUXT_CLOUDFLARE_ACCOUNT_ID=

View File

@ -2,5 +2,6 @@
*.png
*.ico
*.toml
*.patch
https-dev-config/localhost.crt
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.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**
- `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**
- `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}`

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.
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
@ -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.
## Roadmap
[Open board on Volta](https://volta.net/elk-zone/elk)
## Contributing
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">
import type { Account } from 'masto'
import type { mastodon } from 'masto'
defineProps<{
account: Account
account: mastodon.v1.Account
square?: boolean
}>()

View File

@ -1,17 +1,17 @@
<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
// The border is used for Avatar on Avatar for reblogs and connecting replies
defineProps<{
account: Account
account: mastodon.v1.Account
square?: boolean
}>()
</script>
<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" />
</div>
</template>

View File

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

View File

@ -1,5 +1,21 @@
<script setup lang="ts">
defineProps<{
showLabel?: boolean
}>()
</script>
<template>
<div flex="~" items-center border="~ base" text-secondary-light rounded-md px-1 text-xs my-auto>
{{ $t('account.bot') }}
<div
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>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Account } from 'masto'
import type { mastodon } from 'masto'
const { account, as = 'div' } = defineProps<{
account: Account
account: mastodon.v1.Account
as?: string
hoverCard?: boolean
square?: boolean
@ -23,7 +23,7 @@ defineOptions({
<div flex="~ col" shrink pt-1 h-full overflow-hidden justify-center leading-none>
<div flex="~" gap-2>
<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>
<AccountHandle :account="account" text-secondary-light />
</div>

View File

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

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Account } from 'masto'
import type { mastodon } from 'masto'
const { account } = defineProps<{
account: Account
account: mastodon.v1.Account
command?: boolean
}>()
let relationship = $(useRelationship(account))
@ -15,24 +15,31 @@ const toggleMute = async () => {
relationship!.muting = !relationship!.muting
relationship = relationship!.muting
? await masto.accounts.mute(account.id, {
? await masto.v1.accounts.mute(account.id, {
// TODO support more options
})
: await masto.accounts.unmute(account.id)
: await masto.v1.accounts.unmute(account.id)
}
const toggleBlockUser = async () => {
// TODO: Add confirmation
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 () => {
// TODO: Add confirmation
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>
@ -68,6 +75,21 @@ const toggleBlockDomain = async () => {
@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
v-if="!relationship?.muting"
:text="$t('menu.mute_account', [`@${account.acct}`])"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ function close() {
<div>
<slot />
</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 />
</button>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Status } from 'masto'
import type { mastodon } from 'masto'
import type { ConfirmDialogChoice } from '~/types'
import {
isCommandPanelOpen,
@ -30,7 +30,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
}
})
const handlePublished = (status: Status) => {
const handlePublished = (status: mastodon.v1.Status) => {
lastPublishDialogStatus.value = status
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">
<button
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"
>
<div i-ri:arrow-right-s-line text-white />
</button>
<button
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"
>
<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>
<button
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 />
</button>

View File

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

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { buildInfo } from 'virtual:build-info'
const buildInfo = useRuntimeConfig().public.buildInfo
const timeAgoOptions = useTimeAgoOptions()
const buildTimeDate = new Date(buildInfo.time)
@ -16,7 +15,7 @@ function toggleDark() {
<footer p4 text-sm text-secondary-light flex="~ col">
<div flex="~ gap2" items-center mb4>
<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 :content="$t('nav.zen_mode')">
<button

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ const { modelValue } = defineModel<{
:aria-label="$t('settings.notifications.push_notifications.subscription_error.clear_error')"
@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>
</CommonTooltip>
</head>

View File

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

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Attachment, CreateStatusParams, Status, StatusVisibility } from 'masto'
import type { mastodon } from 'masto'
import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core'
import { EditorContent } from '@tiptap/vue-3'
@ -18,13 +18,13 @@ const {
initial?: () => Draft
placeholder?: string
inReplyToId?: string
inReplyToVisibility?: StatusVisibility
inReplyToVisibility?: mastodon.v1.StatusVisibility
expanded?: boolean
dialogLabelledBy?: string
}>()
const emit = defineEmits<{
(evt: 'published', status: Status): void
(evt: 'published', status: mastodon.v1.Status): void
}>()
const { t } = useI18n()
@ -103,7 +103,7 @@ async function uploadAttachments(files: File[]) {
if (draft.attachments.length < limit) {
isExceedingAttachmentLimit = false
try {
const attachment = await masto.mediaAttachments.create({
const attachment = await masto.v1.mediaAttachments.create({
file,
})
draft.attachments.push(attachment)
@ -122,9 +122,9 @@ async function uploadAttachments(files: File[]) {
isUploading = false
}
async function setDescription(att: Attachment, description: string) {
async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
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) {
@ -136,8 +136,8 @@ async function publish() {
...draft.params,
status: htmlToText(draft.params.status || ''),
mediaIds: draft.attachments.map(a => a.id),
...(masto.version.includes('+glitch') ? { 'content-type': 'text/markdown' } : {}),
} as CreateStatusParams
...((masto.config as any).props.version.raw.includes('+glitch') ? { 'content-type': 'text/markdown' } : {}),
} as mastodon.v1.CreateStatusParams
if (process.dev) {
// eslint-disable-next-line no-console
@ -154,11 +154,13 @@ async function publish() {
try {
isSending = true
let status: Status
let status: mastodon.v1.Status
if (!draft.editingStatus)
status = await masto.statuses.create(payload)
status = await masto.v1.statuses.create(payload)
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()
emit('published', status)
@ -251,7 +253,7 @@ defineExpose({
:aria-label="$t('action.clear_upload_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>
</CommonTooltip>
</head>
@ -313,7 +315,7 @@ defineExpose({
<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>
</div>

View File

@ -2,6 +2,8 @@
import { formatTimeAgo } from '@vueuse/core'
const route = useRoute()
const { formatNumber } = useHumanReadableNumber()
const timeAgoOptions = useTimeAgoOptions()
let draftKey = $ref('home')
@ -25,7 +27,7 @@ onMounted(() => {
<div text-right h-8>
<VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end">
<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>
<template #popper="{ hide }">
<div flex="~ col">
@ -38,9 +40,11 @@ onMounted(() => {
>
<div>
<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>
&middot; {{ formatTimeAgo(new Date(draft.lastUpdated)) }}
&middot; {{ formatTimeAgo(new Date(draft.lastUpdated), timeAgoOptions) }}
</span>
</div>
<div text-secondary>

View File

@ -1,20 +1,20 @@
<script setup lang="ts">
import type { Account } from 'masto'
import type { mastodon } from 'masto'
defineProps<{
account: Account
account: mastodon.v1.Account
}>()
</script>
<template>
<button flex gap-2 items-center>
<div flex gap-2 items-center>
<AccountAvatar w-10 h-10 :account="account" shrink-0 />
<div flex="~ col gap1" shrink h-full overflow-hidden leading-none>
<div flex="~" gap-2>
<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>
<AccountHandle text-sm :account="account" text-secondary-light />
</div>
</button>
</div>
</template>

View File

@ -1,7 +1,9 @@
<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(() =>
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 />
</div>
<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>
</template>

View File

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

View File

@ -1,6 +1,4 @@
<script setup lang="ts">
import type { AccountResult, HashTagResult, StatusResult } from './types'
const query = ref('')
const { accounts, hashtags, loading, statuses } = useSearch(query)
const index = ref(0)
@ -15,24 +13,9 @@ const results = computed(() => {
return []
const results = [
...hashtags.value.slice(0, 3).map<HashTagResult>(hashtag => ({
type: 'hashtag',
id: hashtag.id,
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),
})),
...hashtags.value.slice(0, 3),
...accounts.value,
...statuses.value,
// 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 activate = () => {
(document.activeElement as HTMLElement).blur()
const currentIndex = index.value
index.value = -1
if (query.value.length === 0)
return
(document.activeElement as HTMLElement).blur()
// Disable until search page is implemented
// if (currentIndex === -1)
// router.push(`/search?q=${query.value}`)
if (currentIndex === -1)
// router.push(`/search?q=${query.value}`)
return
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">
import type { UpdateCredentialsParams } from 'masto'
import type { mastodon } from 'masto'
const { form } = defineModel<{
form: {
fieldsAttributes: NonNullable<UpdateCredentialsParams['fieldsAttributes']>
fieldsAttributes: NonNullable<mastodon.v1.UpdateCredentialsParams['fieldsAttributes']>
}
}>()
const dropdown = $ref<any>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,26 @@
<script setup lang="ts">
import type { FilterContext, Status } from 'masto'
import type { mastodon } from 'masto'
const props = withDefaults(
defineProps<{
status: Status
status: mastodon.v1.Status
actions?: boolean
context?: FilterContext
context?: mastodon.v2.FilterContext
hover?: boolean
faded?: boolean
// If we know the prev and next status in the timeline, we can simplify the card
older?: Status
newer?: Status
older?: mastodon.v1.Status
newer?: mastodon.v1.Status
// Manual overrides
hasOlder?: boolean
hasNewer?: boolean
// When looking into a detailed view of a post, we can simplify the replying badges
// to the main expanded post
main?: Status
main?: mastodon.v1.Status
}>(),
{ actions: true, showReplyTo: true },
{ actions: true },
)
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)))
// Use reblogged status, connect it to further replies
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 statusRoute = $computed(() => getStatusRoute(status))
const el = ref<HTMLElement>()
const router = useRouter()
@ -47,13 +52,12 @@ function onclick(evt: MouseEvent | KeyboardEvent) {
}
function go(evt: MouseEvent | KeyboardEvent) {
const route = getStatusRoute(status)
if (evt.metaKey || evt.ctrlKey) {
window.open(route.href)
window.open(statusRoute.href)
}
else {
cacheStatus(status)
router.push(route)
router.push(statusRoute)
}
}
@ -63,24 +67,19 @@ const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
// Content Filter logic
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
// 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 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)
// 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 showUpperBorder = $computed(() => props.newer && !directReply)
const showReplyTo = $computed(() => !replyToMain && !directReply)
</script>
<template>
@ -88,8 +87,7 @@ const isDM = $computed(() => status.visibility === 'direct')
v-if="filter?.filterAction !== 'hide'"
:id="`status-${status.id}`"
ref="el"
relative flex flex-col gap-1 pl-3 pr-4 pt-1
class="pb-1.5"
relative flex="~ col gap1" p="l-3 r-4 b-2"
:class="{ 'hover:bg-active': hover }"
tabindex="0"
focus:outline-none focus-visible:ring="2 primary"
@ -97,11 +95,38 @@ const isDM = $computed(() => status.visibility === 'direct')
@click="onclick"
@keydown.enter="onclick"
>
<div v-if="newer && !directReply" w-auto h-1px bg-border />
<div flex justify-between>
<slot name="meta">
<div v-if="rebloggedBy && !collapseRebloggedBy" relative text-secondary ws-nowrap flex="~" items-center pt1 pb0.5 px-1px bg-base>
<div i-ri:repeat-fill me-46px text-primary w-16px h-16px />
<!-- Upper border -->
<div :h="showUpperBorder ? '1px' : '0'" w-auto bg-border mb-1 />
<slot name="meta">
<!-- 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>
<AccountHoverWrapper :account="rebloggedBy">
<NuxtLink :to="getAccountRoute(rebloggedBy)">
@ -111,38 +136,39 @@ const isDM = $computed(() => status.visibility === 'direct')
</div>
<AccountInlineInfo font-bold :account="rebloggedBy" :avatar="false" text-sm />
</div>
<div v-else />
</slot>
<StatusReplyingTo v-if="!directReply && !collapseReplyingTo" :status="status" :simplified="!!simplifyReplyingTo" :class="faded ? 'text-secondary-light' : ''" pt1 />
</div>
</div>
</slot>
<div flex gap-3 :class="{ 'text-secondary': faded }">
<!-- Avatar -->
<div relative>
<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>
<AccountHoverWrapper :account="status.account">
<NuxtLink :to="getAccountRoute(status.account)" rounded-full>
<AccountBigAvatar :account="status.account" />
</NuxtLink>
</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>
<!-- Main -->
<div flex="~ col 1" min-w-0>
<!-- Account Info -->
<div flex items-center space-x-1>
<AccountHoverWrapper :account="status.account">
<StatusAccountDetails :account="status.account" />
</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 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 />
<div flex>
<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">
{{ timeago }}
</time>
@ -153,10 +179,10 @@ const isDM = $computed(() => status.visibility === 'direct')
</div>
<StatusActionsMore v-if="actions !== false" :status="status" me--2 />
</div>
<!-- Content -->
<StatusContent :status="status" :context="context" mb2 :class="{ 'mt-2 mb1': isDM }" />
<div>
<StatusActions v-if="(actions !== false && !userSettings.zenMode)" :status="status" />
</div>
<StatusActions v-if="actions !== false" v-show="!userSettings.zenMode" :status="status" />
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Status } from 'masto'
import type { mastodon } from 'masto'
const { status } = defineProps<{
status: Status
status: mastodon.v1.Status
}>()
const poll = reactive({ ...status.poll! })
@ -30,7 +30,7 @@ async function vote(e: Event) {
poll.votersCount = (poll.votersCount || 0) + 1
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)

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Card, CardType } from 'masto'
import type { mastodon } from 'masto'
const props = defineProps<{
card: Card
card: mastodon.v1.PreviewCard
/** For the preview image, only the small image mode is displayed */
smallPictureOnly?: boolean
/** 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'))
// TODO: handle card.type: 'photo' | 'video' | 'rich';
const cardTypeIconMap: Record<CardType, string> = {
const cardTypeIconMap: Record<mastodon.v1.PreviewCardType, string> = {
link: 'i-ri:profile-line',
photo: 'i-ri:image-line',
video: 'i-ri:play-line',

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ watchEffect(() => {
<template>
<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" />
</div>
<div flex="~ gap-1 center" w-full mt="-4.5">

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { Tag } from 'masto'
import type { mastodon } from 'masto'
const {
tag,
} = $defineProps<{
tag: Tag
tag: mastodon.v1.Tag
}>()
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">
const paginator = useMasto().blocks.iterate()
const paginator = useMasto().v1.blocks.list()
</script>
<template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
// 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()
onActivated(clearNotifications)
const stream = useMasto().stream.streamUser()
const stream = useMasto().v1.stream.streamUser()
</script>
<template>

View File

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

View File

@ -1,5 +1,5 @@
<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>
<template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import type { BuildInfo } from '~~/types'
export interface Team {
github: string
display: string
@ -31,3 +33,7 @@ export const teams: Team[] = [
mastodon: 'sxzz@webtoo.ls',
},
].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 type { Account, Status } from 'masto'
import type { mastodon } from 'masto'
const cache = new LRU<string, any>({
max: 1000,
@ -17,13 +17,13 @@ function removeCached(key: string) {
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 key = `${server}:status:${id}`
const cached = cache.get(key)
if (cached && !force)
return cached
const promise = useMasto().statuses.fetch(id)
const promise = useMasto().v1.statuses.fetch(id)
.then((status) => {
cacheStatus(status)
return status
@ -32,7 +32,7 @@ export function fetchStatus(id: string, force = false): Promise<Status> {
return promise
}
export function fetchAccountById(id?: string | null): Promise<Account | null> {
export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Account | null> {
if (!id)
return Promise.resolve(null)
@ -41,11 +41,11 @@ export function fetchAccountById(id?: string | null): Promise<Account | null> {
const cached = cache.get(key)
if (cached)
return cached
const uri = currentInstance.value?.uri
const promise = useMasto().accounts.fetch(id)
const domain = currentInstance.value?.uri
const promise = useMasto().v1.accounts.fetch(id)
.then((r) => {
if (r.acct && !r.acct.includes('@') && uri)
r.acct = `${r.acct}@${uri}`
if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${domain}`
cacheAccount(r, server, true)
return r
@ -54,17 +54,17 @@ export function fetchAccountById(id?: string | null): Promise<Account | null> {
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 key = `${server}:account:${acct}`
const cached = cache.get(key)
if (cached)
return cached
const uri = currentInstance.value?.uri
const account = useMasto().accounts.lookup({ acct })
const domain = currentInstance.value?.uri
const account = useMasto().v1.accounts.lookup({ acct })
.then((r) => {
if (r.acct && !r.acct.includes('@') && uri)
r.acct = `${r.acct}@${uri}`
if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${domain}`
cacheAccount(r, server, true)
return r
@ -81,7 +81,7 @@ export function useAccountById(id?: string | null) {
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)
}
@ -89,7 +89,7 @@ export function removeCachedStatus(id: string, server = currentServer.value) {
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.acct}`, account, override)
}

View File

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

View File

@ -1,15 +1,16 @@
// @unimport-disable
import type { Emoji } from 'masto'
import type { mastodon } from 'masto'
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 { emojiRegEx, getEmojiAttributes } from '../config/emojis'
export interface ContentParseOptions {
emojis?: Record<string, Emoji>
emojis?: Record<string, mastodon.v1.CustomEmoji>
markdown?: boolean
replaceUnicodeEmoji?: boolean
astTransforms?: Transform[]
convertMentionLink?: boolean
}
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 {
markdown = true,
replaceUnicodeEmoji = true,
convertMentionLink = false,
} = options
if (markdown) {
@ -77,19 +79,25 @@ export function parseMastodonHTML(
if (markdown)
transforms.push(transformMarkdown)
if (convertMentionLink)
transforms.push(transformMentionLink)
transforms.push(replaceCustomEmoji(options.emojis || {}))
transforms.push(transformParagraphs)
return transformSync(parse(html), transforms)
}
/**
* 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, {
emojis: customEmojis,
markdown: true,
replaceUnicodeEmoji: false,
convertMentionLink: true,
})
return render(tree)
}
@ -285,7 +293,7 @@ function transformUnicodeEmoji(node: Node) {
return matches.filter(Boolean)
}
function replaceCustomEmoji(customEmojis: Record<string, Emoji>): Transform {
function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => {
if (node.type !== TEXT_NODE)
return node
@ -313,6 +321,8 @@ const _markdownReplacements: [RegExp, (c: (string | Node)[]) => Node][] = [
[/\*(.*?)\*/g, c => h('em', null, c)],
[/~~(.*?)~~/g, c => h('del', 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) {
@ -349,3 +359,26 @@ function transformMarkdown(node: Node) {
return node
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 { STORAGE_KEY_FIRST_VISIT } from '~/constants'
export const confirmDialogChoice = ref<ConfirmDialogChoice>()
export const confirmDialogLabel = ref<ConfirmDialogLabel>()
export const mediaPreviewList = ref<Attachment[]>([])
export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
export const mediaPreviewIndex = ref(0)
export const statusEdit = ref<StatusEdit>()
export const statusEdit = ref<mastodon.v1.StatusEdit>()
export const dialogDraftKey = ref<string>()
export const commandPanelInput = ref('')
@ -23,7 +23,7 @@ export const isPreviewHelpOpen = ref(isFirstVisit.value)
export const isCommandPanelOpen = 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() {
isSigninDialogOpen.value = true
@ -80,7 +80,7 @@ if (process.client) {
restoreMediaPreviewFromState()
}
export function openMediaPreview(attachments: Attachment[], index = 0) {
export function openMediaPreview(attachments: mastodon.v1.MediaAttachment[], index = 0) {
mediaPreviewList.value = attachments
mediaPreviewIndex.value = index
isMediaPreviewOpen.value = true
@ -97,7 +97,7 @@ export function closeMediaPreview() {
history.back()
}
export function openEditHistoryDialog(edit: StatusEdit) {
export function openEditHistoryDialog(edit: mastodon.v1.StatusEdit) {
statusEdit.value = edit
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 { STORAGE_KEY_CUSTOM_EMOJIS } from '~/constants'
@ -20,14 +20,14 @@ export async function updateCustomEmojis() {
return
const masto = useMasto()
const emojis = await masto.customEmojis.fetchAll()
const emojis = await masto.v1.customEmojis.list()
Object.assign(currentCustomEmojis.value, {
lastUpdate: Date.now(),
emojis,
})
}
function transformEmojiData(emojis: Emoji[]) {
function transformEmojiData(emojis: mastodon.v1.CustomEmoji[]) {
const result = []
for (const emoji of emojis) {
@ -52,9 +52,9 @@ export const customEmojisData = computed(() => currentCustomEmojis.value.emojis.
}]
: undefined)
export function useEmojisFallback(emojisGetter: () => Emoji[] | undefined) {
export function useEmojisFallback(emojisGetter: () => mastodon.v1.CustomEmoji[] | undefined) {
return computed(() => {
const result: Emoji[] = []
const result: mastodon.v1.CustomEmoji[] = []
const emojis = emojisGetter()
if (emojis)
result.push(...emojis)

View File

@ -2,11 +2,11 @@ import type { MaybeComputedRef, MaybeRef, UseTimeAgoOptions } from '@vueuse/core
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)
}
export const useHumanReadableNumber = () => {
export function useHumanReadableNumber() {
const { n, locale } = useI18n()
const fn = (num: number) => {
@ -29,10 +29,8 @@ export const useHumanReadableNumber = () => {
}
}
export const useFormattedDateTime = (
value: MaybeComputedRef<string | number | Date | undefined | null>,
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' },
) => {
export function useFormattedDateTime(value: MaybeComputedRef<string | number | Date | undefined | null>,
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' }) {
const { locale } = useI18n()
const formatter = $computed(() => Intl.DateTimeFormat(locale.value, options))
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 prefix = short ? 'short_' : ''
@ -56,7 +54,7 @@ export const useTimeAgoOptions = (short = false): UseTimeAgoOptions<false> => {
return {
rounding: 'floor',
showSecond: !short,
updateInterval: short ? 60_000 : 1_000,
updateInterval: short ? 60000 : 1000,
messages: {
justNow: t('time_ago_options.just_now'),
// 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 || ''
if (options?.rich)
return displayName
return displayName.replace(/:([\w-]+?):/g, '')
}
export function getShortHandle({ acct }: Account) {
export function getShortHandle({ acct }: mastodon.v1.Account) {
if (!acct)
return ''
return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
}
export function getServerName(account: Account) {
export function getServerName(account: mastodon.v1.Account) {
if (account.acct?.includes('@'))
return account.acct.split('@')[1]
// We should only lack the server name if we're on the same server as the account
return currentInstance.value?.uri || ''
}
export function getFullHandle(account: Account) {
export function getFullHandle(account: mastodon.v1.Account) {
const handle = `@${account.acct}`
if (!currentUser.value || account.acct.includes('@'))
return handle
@ -36,7 +36,7 @@ export function toShortHandle(fullHandle: string) {
return fullHandle
}
export function extractAccountHandle(account: Account) {
export function extractAccountHandle(account: mastodon.v1.Account) {
let handle = getFullHandle(account).slice(1)
const uri = currentInstance.value?.uri ?? currentServer.value
if (currentInstance.value && handle.endsWith(`@${uri}`))
@ -45,7 +45,7 @@ export function extractAccountHandle(account: Account) {
return handle
}
export function useAccountHandle(account: Account, fullServer = true) {
export function useAccountHandle(account: mastodon.v1.Account, fullServer = true) {
return computed(() => fullServer
? getFullHandle(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'
// 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
// 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
export function useRelationship(account: Account): Ref<Relationship | undefined> {
export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.Relationship | undefined> {
if (!currentUser.value)
return ref()
let relationship = requestedRelationships.get(account.id)
if (relationship)
return relationship
relationship = ref<Relationship | undefined>()
relationship = ref<mastodon.v1.Relationship | undefined>()
requestedRelationships.set(account.id, relationship)
if (timeoutHandle)
clearTimeout(timeoutHandle)
@ -27,7 +27,7 @@ export function useRelationship(account: Account): Ref<Relationship | undefined>
async function fetchRelationships() {
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++)
requested[i][1].value = relationships[i]
}

View File

@ -1,7 +1,7 @@
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({
name: 'account-index',
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({
name: 'account-following',
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({
name: 'account-followers',
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({
name: 'status',
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
}
export function getStatusInReplyToRoute(status: Status) {
export function getStatusInReplyToRoute(status: mastodon.v1.Status) {
return useRouter().resolve({
name: 'status-by-id',
params: {

View File

@ -1,19 +1,64 @@
import type { MaybeRef } from '@vueuse/core'
import type { Account, Paginator, Results, SearchParams, Status } from 'masto'
import type { MaybeComputedRef } from '@vueuse/core'
import type { Paginator, mastodon } from 'masto'
import type { RouteLocation } from 'vue-router'
export interface UseSearchOptions {
type?: MaybeRef<'accounts' | 'hashtags' | 'statuses'>
export type UseSearchOptions = MaybeComputedRef<
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 masto = useMasto()
const loading = ref(false)
const statuses = ref<Status[]>([])
const accounts = ref<Account[]>([])
const hashtags = ref<any[]>([])
const accounts = ref<AccountSearchResult[]>([])
const hashtags = ref<HashTagSearchResult[]>([])
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 () => {
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,
* 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()
done.value = nextResults.done || false
statuses.value = nextResults.value?.statuses || []
accounts.value = nextResults.value?.accounts || []
hashtags.value = nextResults.value?.hashtags || []
done.value = !!nextResults.done
if (!nextResults.done)
appendResults(nextResults.value, true)
loading.value = false
}, { debounce: 500 })
}, { debounce: 300 })
const next = async () => {
if (!unref(query) || !isMastoInitialised.value || !paginator)
@ -45,19 +92,9 @@ export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) {
const nextResults = await paginator.next()
loading.value = false
done.value = nextResults.done || false
statuses.value = [
...statuses.value,
...(nextResults.value.statuses || []),
]
accounts.value = [
...statuses.value,
...(nextResults.value.accounts || []),
]
hashtags.value = [
...statuses.value,
...(nextResults.value.statuses || []),
]
done.value = !!nextResults.done
if (!nextResults.done)
appendResults(nextResults.value)
}
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 CountField = 'reblogsCount' | 'favouritesCount'
export interface StatusActionsProps {
status: Status
status: mastodon.v1.Status
}
export function useStatusActions(props: StatusActionsProps) {
let status = $ref<Status>({ ...props.status })
let status = $ref<mastodon.v1.Status>({ ...props.status })
const masto = useMasto()
watch(
@ -27,10 +27,11 @@ export function useStatusActions(props: StatusActionsProps) {
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
if (!checkLogin())
return
isLoading[action] = true
fetchNewStatus().then((newStatus) => {
Object.assign(status, newStatus)
@ -44,9 +45,15 @@ export function useStatusActions(props: StatusActionsProps) {
if (countField)
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(
'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)
// returns the original status
return res.reblog!
@ -57,28 +64,29 @@ export function useStatusActions(props: StatusActionsProps) {
const toggleFavourite = () => toggleStatusAction(
'favourited',
() => masto.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
() => masto.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
'favouritesCount',
)
const toggleBookmark = () => toggleStatusAction(
'bookmarked',
() => masto.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
() => masto.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
)
const togglePin = async () => toggleStatusAction(
'pinned',
() => masto.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
() => masto.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
)
const toggleMute = async () => toggleStatusAction(
'muted',
() => masto.statuses[status.muted ? 'unmute' : 'mute'](status.id),
() => masto.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id),
)
return {
status: $$(status),
isLoading: $$(isLoading),
canReblog: $$(canReblog),
toggleMute,
toggleReblog,
toggleFavourite,

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