forked from Mirrors/elk
Merge branch 'main' into fix/avatar-outline
This commit is contained in:
commit
30a4bed80f
162 changed files with 1140 additions and 896 deletions
|
@ -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=
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}`
|
||||||
|
|
11
README.md
11
README.md
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
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') }}
|
{{ $t('account.bot') }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
|
@ -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}`])"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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(() =>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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) } }) }} <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>
|
||||||
· {{ formatTimeAgo(new Date(draft.lastUpdated)) }}
|
· {{ formatTimeAgo(new Date(draft.lastUpdated), timeAgoOptions) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div text-secondary>
|
<div text-secondary>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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> -->
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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()"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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()"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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">
|
<slot name="meta">
|
||||||
<div v-if="rebloggedBy && !collapseRebloggedBy" relative text-secondary ws-nowrap flex="~" items-center pt1 pb0.5 px-1px bg-base>
|
<!-- Line connecting to previous status -->
|
||||||
<div i-ri:repeat-fill me-46px text-primary w-16px h-16px />
|
<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 />
|
|
||||||
</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 }">
|
<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>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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="~" items-center h-auto font-bold text-sm text-secondary gap-1
|
flex="~ gap2" items-center h-auto text-sm text-secondary
|
||||||
:to="getStatusInReplyToRoute(status)"
|
:to="getStatusInReplyToRoute(status)"
|
||||||
:title="account ? `Replying to ${getDisplayName(account)}` : 'Replying to someone'"
|
: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">
|
<template v-if="account">
|
||||||
<div i-ri:reply-fill :class="collapsed ? '' : 'scale-x-[-1]'" text-secondary-light />
|
<AccountInlineInfo :account="account" :link="false" />
|
||||||
<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 />
|
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('status.someone') }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</template>
|
</template>
|
||||||
<div i-ri:question-answer-line text-secondary-light text-lg />
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
<template #default="{ items, item, index }">
|
||||||
<CommonDropdownItem
|
<CommonDropdownItem
|
||||||
v-for="(edit, idx) in statusEdits"
|
|
||||||
:key="idx"
|
|
||||||
px="0.5"
|
px="0.5"
|
||||||
@click="showHistory(edit)"
|
@click="showHistory(item)"
|
||||||
>
|
>
|
||||||
{{ getDisplayName(edit.account) }}
|
{{ getDisplayName(item.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>
|
</i18n-t>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<i18n-t v-else keypath="status_history.edited">
|
||||||
<i18n-t keypath="status_history.edited">
|
{{ formatTimeAgo(new Date(item.createdAt), timeAgoOptions) }}
|
||||||
{{ formatTimeAgo(new Date(edit.createdAt), timeAgoOptions) }}
|
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</template>
|
|
||||||
</CommonDropdownItem>
|
</CommonDropdownItem>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template #loading>
|
||||||
<div i-ri:loader-2-fill animate-spin text-2xl ma />
|
<StatusEditHistorySkeleton />
|
||||||
|
<StatusEditHistorySkeleton op50 />
|
||||||
|
<StatusEditHistorySkeleton op25 />
|
||||||
</template>
|
</template>
|
||||||
|
<template #done>
|
||||||
|
<span />
|
||||||
|
</template>
|
||||||
|
</CommonPaginator>
|
||||||
</template>
|
</template>
|
||||||
|
|
3
components/status/edit/StatusEditHistorySkeleton.vue
Normal file
3
components/status/edit/StatusEditHistorySkeleton.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div class="skeleton-loading-bg" h-5 w-full rounded my2 />
|
||||||
|
</template>
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
22
components/tag/TagCardPaginator.vue
Normal file
22
components/tag/TagCardPaginator.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue