Merge branch 'main' into userquin/feat-remember-last-position

This commit is contained in:
userquin 2023-01-08 16:24:06 +01:00
commit cd03f3fa1e
58 changed files with 459 additions and 293 deletions

View file

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

View file

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

View file

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

View file

@ -8,8 +8,9 @@ defineProps<{
<div
flex="~ gap1" items-center
:class="{ 'border border-base rounded-md px-1': showLabel }"
text-secondary-light text-xs
text-secondary-light
>
<slot name="prepend" />
<CommonTooltip :content="$t('account.bot')" :disabled="showLabel">
<div i-ri:robot-line />
</CommonTooltip>

View file

@ -23,7 +23,7 @@ defineOptions({
<div flex="~ col" shrink pt-1 h-full overflow-hidden justify-center leading-none>
<div flex="~" gap-2>
<AccountDisplayName :account="account" font-bold line-clamp-1 ws-pre-wrap break-all text-lg />
<AccountBotIndicator v-if="account.bot" />
<AccountBotIndicator v-if="account.bot" text-xs />
</div>
<AccountHandle :account="account" text-secondary-light />
</div>

View file

@ -34,6 +34,13 @@ const toggleBlockDomain = async () => {
relationship!.domainBlocking = !relationship!.domainBlocking
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>
<template>
@ -68,6 +75,21 @@ const toggleBlockDomain = async () => {
@click="directMessageUser(account)"
/>
<CommonDropdownItem
v-if="!relationship?.showingReblogs"
icon="i-ri:repeat-line"
:text="$t('menu.show_reblogs', [`@${account.acct}`])"
:command="command"
@click="toggleReblogs"
/>
<CommonDropdownItem
v-else
:text="$t('menu.hide_reblogs', [`@${account.acct}`])"
icon="i-ri:repeat-line"
:command="command"
@click="toggleReblogs"
/>
<CommonDropdownItem
v-if="!relationship?.muting"
:text="$t('menu.mute_account', [`@${account.acct}`])"

View file

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

View file

@ -1,4 +1,4 @@
<script setup lang="ts" generic="T extends any, O extends any">
<script setup lang="ts" generic="T, O">
// @ts-expect-error missing types
import { DynamicScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
@ -22,7 +22,9 @@ const {
defineSlots<{
default: {
items: T[]
item: T
index: number
active?: boolean
older?: T
newer?: T // newer is undefined when index === 0
@ -61,6 +63,8 @@ const { items, prevItems, update, state, endAnchor, error } = usePaginator(pagin
:active="active"
:older="items[index + 1]"
:newer="items[index - 1]"
:index="index"
:items="items"
/>
</DynamicScroller>
</template>
@ -71,6 +75,8 @@ const { items, prevItems, update, state, endAnchor, error } = usePaginator(pagin
:item="item"
:older="items[index + 1]"
:newer="items[index - 1]"
:index="index"
:items="items"
/>
</template>
</slot>

View file

@ -1,6 +1,5 @@
<script setup lang="ts">
import { buildInfo } from 'virtual:build-info'
const buildInfo = useRuntimeConfig().public.buildInfo
const timeAgoOptions = useTimeAgoOptions()
const buildTimeDate = new Date(buildInfo.time)

View file

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

View file

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

View file

@ -12,7 +12,7 @@ defineProps<{
<div flex="~ col gap1" shrink h-full overflow-hidden leading-none>
<div flex="~" gap-2>
<AccountDisplayName :account="account" line-clamp-1 ws-pre-wrap break-all text-base />
<AccountBotIndicator v-if="account.bot" />
<AccountBotIndicator v-if="account.bot" text-xs />
</div>
<AccountHandle text-sm :account="account" text-secondary-light />
</div>

View file

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

View file

@ -15,6 +15,7 @@ const { details, command } = $(props)
const {
status,
isLoading,
canReblog,
toggleBookmark,
toggleFavourite,
toggleReblog,
@ -39,7 +40,7 @@ const reply = () => {
:content="$t('action.reply')"
:text="status.repliesCount || ''"
color="text-blue" hover="text-blue" group-hover="bg-blue/10"
icon="i-ri:chat-3-line"
icon="i-ri:chat-1-line"
:command="command"
@click="reply"
>
@ -63,7 +64,7 @@ const reply = () => {
icon="i-ri:repeat-line"
active-icon="i-ri:repeat-fill"
:active="!!status.reblogged"
:disabled="isLoading.reblogged"
:disabled="isLoading.reblogged || !canReblog"
:command="command"
@click="toggleReblog()"
>

View file

@ -129,7 +129,7 @@ async function editStatus() {
<template v-if="userSettings.zenMode">
<CommonDropdownItem
:text="$t('action.reply')"
icon="i-ri:chat-3-line"
icon="i-ri:chat-1-line"
:command="command"
@click="reply()"
/>

View file

@ -65,14 +65,23 @@ const video = ref<HTMLVideoElement | undefined>()
const prefersReducedMotion = usePreferredReducedMotion()
useIntersectionObserver(video, (entries) => {
if (prefersReducedMotion.value === 'reduce')
const ready = video.value?.dataset.ready === 'true'
if (prefersReducedMotion.value === 'reduce') {
if (ready && !video.value?.paused)
video.value?.pause()
return
}
entries.forEach((entry) => {
if (entry.intersectionRatio <= 0.75)
!video.value!.paused && video.value!.pause()
else
video.value!.play()
if (entry.intersectionRatio <= 0.75) {
ready && !video.value?.paused && video.value?.pause()
}
else {
video.value?.play().then(() => {
video.value!.dataset.ready = 'true'
}).catch(noop)
}
})
}, { threshold: 0.75 })
</script>

View file

@ -99,35 +99,35 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
<!-- Upper border -->
<div :h="showUpperBorder ? '1px' : '0'" w-auto bg-border mb-1 />
<!-- Line connecting to previous status -->
<template v-if="status.inReplyToAccountId">
<StatusReplyingTo
v-if="showReplyTo"
ml-6 pt-1 pl-5
:status="status"
:is-self-reply="isSelfReply"
:class="faded ? 'text-secondary-light' : ''"
/>
<div flex="~ col gap-1" items-center pos="absolute top-0 left-0" w="20.5" z--1>
<template v-if="showReplyTo">
<div w="1px" h="0.5" border="x base" mt-3 />
<div w="1px" h="0.5" border="x base" />
<div w="1px" h="0.5" border="x base" />
</template>
<div w="1px" h-10 border="x base" />
</div>
</template>
<slot name="meta">
<!-- Line connecting to previous status -->
<template v-if="status.inReplyToAccountId">
<StatusReplyingTo
v-if="showReplyTo"
ml-6 pt-1 pl-5
:status="status"
:is-self-reply="isSelfReply"
:class="faded ? 'text-secondary-light' : ''"
/>
<div flex="~ col gap-1" items-center pos="absolute top-0 left-0" w="20.5" z--1>
<template v-if="showReplyTo">
<div w="1px" h="0.5" border="x base" mt-3 />
<div w="1px" h="0.5" border="x base" />
<div w="1px" h="0.5" border="x base" />
</template>
<div w="1px" h-10 border="x base" />
</div>
</template>
<!-- Reblog status & Meta -->
<div flex="~ col" justify-between>
<slot name="meta">
<!-- 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-primary w-16px h-16px />
<div i-ri:repeat-fill me-46px text-green w-16px h-16px />
<div absolute top-1 ms-24px w-32px h-32px rounded-full>
<AccountHoverWrapper :account="rebloggedBy">
<NuxtLink :to="getAccountRoute(rebloggedBy)">
@ -137,14 +137,14 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
</div>
<AccountInlineInfo font-bold :account="rebloggedBy" :avatar="false" text-sm />
</div>
</slot>
</div>
</div>
</slot>
<div flex gap-3 :class="{ 'text-secondary': faded }">
<!-- Avatar -->
<div relative>
<div v-if="collapseRebloggedBy" absolute flex items-center justify-center top--6px px-2px py-3px rounded-full bg-base>
<div i-ri:repeat-fill text-primary w-16px h-16px />
<div i-ri:repeat-fill text-green w-16px h-16px />
</div>
<AccountHoverWrapper :account="status.account">
<NuxtLink :to="getAccountRoute(status.account)" rounded-full>

View file

@ -18,16 +18,18 @@ const account = isSelf ? computed(() => status.account) : useAccountById(status.
v-if="status.inReplyToId"
flex="~ gap2" items-center h-auto text-sm text-secondary
:to="getStatusInReplyToRoute(status)"
:title=" $t('status.replying_to', [account ? getDisplayName(account) : $t('status.someone')])"
:title="$t('status.replying_to', [account ? getDisplayName(account) : $t('status.someone')])"
text-blue saturate-50 hover:saturate-100
>
<template v-if="isSelfReply">
<span btn-text p0 mb-1>{{ $t('status.show_full_thread') }}</span>
<div i-ri-discuss-line text-blue />
<span>{{ $t('status.show_full_thread') }}</span>
</template>
<template v-else>
<div i-ri-chat-1-line />
<div i-ri-chat-1-line text-blue />
<i18n-t keypath="status.replying_to">
<template v-if="account">
<AccountInlineInfo :account="account" :link="false" mx1 />
<AccountInlineInfo :account="account" :link="false" />
</template>
<template v-else>
{{ $t('status.someone') }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
const cached = cache.get(key)
if (cached)
return cached
const domain = currentInstance.value?.domain
const domain = currentInstance.value?.uri
const promise = useMasto().v1.accounts.fetch(id)
.then((r) => {
if (r.acct && !r.acct.includes('@') && domain)
@ -60,7 +60,7 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
const cached = cache.get(key)
if (cached)
return cached
const domain = currentInstance.value?.domain
const domain = currentInstance.value?.uri
const account = useMasto().v1.accounts.lookup({ acct })
.then((r) => {
if (r.acct && !r.acct.includes('@') && domain)

View file

@ -10,6 +10,7 @@ export interface ContentParseOptions {
markdown?: boolean
replaceUnicodeEmoji?: boolean
astTransforms?: Transform[]
convertMentionLink?: boolean
}
const sanitizerBasicClasses = filterClasses(/^(h-\S*|p-\S*|u-\S*|dt-\S*|e-\S*|mention|hashtag|ellipsis|invisible)$/u)
@ -53,6 +54,7 @@ export function parseMastodonHTML(
const {
markdown = true,
replaceUnicodeEmoji = true,
convertMentionLink = false,
} = options
if (markdown) {
@ -77,6 +79,9 @@ export function parseMastodonHTML(
if (markdown)
transforms.push(transformMarkdown)
if (convertMentionLink)
transforms.push(transformMentionLink)
transforms.push(replaceCustomEmoji(options.emojis || {}))
transforms.push(transformParagraphs)
@ -92,6 +97,7 @@ export function convertMastodonHTML(html: string, customEmojis: Record<string, m
emojis: customEmojis,
markdown: true,
replaceUnicodeEmoji: false,
convertMentionLink: true,
})
return render(tree)
}
@ -316,7 +322,7 @@ const _markdownReplacements: [RegExp, (c: (string | Node)[]) => Node][] = [
[/~~(.*?)~~/g, c => h('del', null, c)],
[/`([^`]+?)`/g, c => h('code', null, c)],
// transform @username@twitter.com as links
[/(?:^|\b)@([a-zA-Z0-9_]+)@twitter\.com(?:$|\b)/gi, c => h('a', { href: `https://twitter.com/${c[0]}`, target: '_blank', class: 'mention external' }, `@${c[0]}@twitter.com`)],
[/\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) {
@ -360,3 +366,19 @@ function transformParagraphs(node: Node): Node | Node[] {
return [node, h('p')]
return node
}
function transformMentionLink(node: Node): string | Node | (string | Node)[] | null {
if (node.name === 'a' && node.attributes.class?.includes('mention')) {
const href = node.attributes.href
if (href) {
const matchUser = href.match(UserLinkRE)
if (matchUser) {
const [, server, username] = matchUser
const handle = `${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
// convert to TipTap mention node
return h('span', { 'data-type': 'mention', 'data-id': handle }, handle)
}
}
}
return node
}

View file

@ -17,7 +17,7 @@ export function getServerName(account: mastodon.v1.Account) {
if (account.acct?.includes('@'))
return account.acct.split('@')[1]
// We should only lack the server name if we're on the same server as the account
return currentInstance.value?.domain || ''
return currentInstance.value?.uri || ''
}
export function getFullHandle(account: mastodon.v1.Account) {
@ -38,7 +38,7 @@ export function toShortHandle(fullHandle: string) {
export function extractAccountHandle(account: mastodon.v1.Account) {
let handle = getFullHandle(account).slice(1)
const uri = currentInstance.value?.domain ?? currentServer.value
const uri = currentInstance.value?.uri ?? currentServer.value
if (currentInstance.value && handle.endsWith(`@${uri}`))
handle = handle.slice(0, -uri.length - 1)

View file

@ -1,10 +1,10 @@
import type { MaybeRef } from '@vueuse/core'
import type { MaybeComputedRef } from '@vueuse/core'
import type { Paginator, mastodon } from 'masto'
import type { RouteLocation } from 'vue-router'
export interface UseSearchOptions {
type?: MaybeRef<mastodon.v2.SearchType>
}
export type UseSearchOptions = MaybeComputedRef<
Partial<Omit<mastodon.v1.SearchParams, keyof mastodon.DefaultPaginationParams | 'q'>>
>
export interface BuildSearchResult<K extends keyof any, T> {
id: string
@ -20,7 +20,7 @@ export type StatusSearchResult = BuildSearchResult<'status', mastodon.v1.Status>
export type SearchResult = HashTagSearchResult | AccountSearchResult | StatusSearchResult
export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) {
export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOptions = {}) {
const done = ref(false)
const masto = useMasto()
const loading = ref(false)
@ -71,9 +71,9 @@ export function useSearch(query: MaybeRef<string>, options?: UseSearchOptions) {
* but that doesn't seem to be the case. So instead we just create a new paginator with the new params.
*/
paginator = masto.v2.search({
q: unref(query),
q: resolveUnref(query),
...resolveUnref(options),
resolve: !!currentUser.value,
type: unref(options?.type),
})
const nextResults = await paginator.next()

View file

@ -31,6 +31,7 @@ export function useStatusActions(props: StatusActionsProps) {
// check login
if (!checkLogin())
return
isLoading[action] = true
fetchNewStatus().then((newStatus) => {
Object.assign(status, newStatus)
@ -44,6 +45,12 @@ export function useStatusActions(props: StatusActionsProps) {
if (countField)
status[countField] += status[action] ? 1 : -1
}
const canReblog = $computed(() =>
status.visibility !== 'direct'
&& (status.visibility !== 'private' || status.account.id === currentUser.value?.account.id),
)
const toggleReblog = () => toggleStatusAction(
'reblogged',
() => masto.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => {
@ -79,6 +86,7 @@ export function useStatusActions(props: StatusActionsProps) {
return {
status: $$(status),
isLoading: $$(isLoading),
canReblog: $$(canReblog),
toggleMute,
toggleReblog,
toggleFavourite,

View file

@ -54,12 +54,17 @@ function mentionHTML(acct: string) {
return `<span data-type="mention" data-id="${acct}" contenteditable="false">@${acct}</span>`
}
export function getReplyDraft(status: mastodon.v1.Status) {
const accountsToMention: string[] = []
function getAccountsToMention(status: mastodon.v1.Status) {
const userId = currentUser.value?.account.id
const accountsToMention: string[] = []
if (status.account.id !== userId)
accountsToMention.push(status.account.acct)
accountsToMention.push(...(status.mentions.filter(mention => mention.id !== userId).map(mention => mention.acct)))
return accountsToMention
}
export function getReplyDraft(status: mastodon.v1.Status) {
const accountsToMention = getAccountsToMention(status)
return {
key: `reply-${status.id}`,
draft: () => {
@ -77,7 +82,9 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
return true
const { params, attachments } = draft
const status = params.status || ''
return (status.length === 0 || status === '<p></p>')
const text = htmlToText(status).trim().replace(/^(@\S+\s?)+/, '').trim()
return (text.length === 0)
&& attachments.length === 0
&& (params.spoilerText || '').length === 0
}

View file

@ -1,9 +1,9 @@
import type { Directions } from 'vue-i18n-routing'
import { buildInfo } from 'virtual:build-info'
import type { LocaleObject } from '#i18n'
export function setupPageHeader() {
const { locale, locales, t } = useI18n()
const buildInfo = useBuildInfo()
const localeMap = (locales.value as LocaleObject[]).reduce((acc, l) => {
acc[l.code!] = l.dir ?? 'auto'

View file

@ -29,7 +29,13 @@ export const HashtagSuggestion: Partial<SuggestionOptions> = {
if (query.length === 0)
return []
const paginator = useMasto().v2.search({ q: query, type: 'hashtags', limit: 25, resolve: true })
const paginator = useMasto().v2.search({
q: query,
type: 'hashtags',
limit: 25,
resolve: false,
excludeUnreviewed: true,
})
const results = await paginator.next()
return results.value.hashtags

View file

@ -40,7 +40,7 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
}
const users = await initializeUsers()
const instances = useLocalStorage<Record<string, mastodon.v2.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
export const currentUser = computed<UserLogin | undefined>(() => {
@ -53,8 +53,8 @@ export const currentUser = computed<UserLogin | undefined>(() => {
return users.value[0]
})
const publicInstance = ref<mastodon.v2.Instance | null>(null)
export const currentInstance = computed<null | mastodon.v2.Instance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value)
const publicInstance = ref<mastodon.v1.Instance | null>(null)
export const currentInstance = computed<null | mastodon.v1.Instance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value)
export const publicServer = ref('')
export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value)
@ -91,7 +91,7 @@ if (process.client) {
}
export const currentUserHandle = computed(() => currentUser.value?.account.id
? `${currentUser.value.account.acct}@${currentInstance.value?.domain || currentServer.value}`
? `${currentUser.value.account.acct}@${currentInstance.value?.uri || currentServer.value}`
: '[anonymous]',
)
@ -111,14 +111,14 @@ async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: mastodon.
if (!user?.token) {
publicServer.value = server
publicInstance.value = await masto.v2.instance.fetch()
publicInstance.value = await masto.v1.instances.fetch()
}
else {
try {
const [me, instance, pushSubscription] = await Promise.all([
masto.v1.accounts.verifyCredentials(),
masto.v2.instance.fetch(),
masto.v1.instances.fetch(),
// if PWA is not enabled, don't get push subscription
useRuntimeConfig().public.pwaEnabled
// we get 404 response instead empty data
@ -127,7 +127,7 @@ async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: mastodon.
])
if (!me.acct.includes('@'))
me.acct = `${me.acct}@${instance.domain}`
me.acct = `${me.acct}@${instance.uri}`
user.account = me
user.pushSubscription = pushSubscription
@ -169,7 +169,7 @@ export function setAccountInfo(userId: string, account: mastodon.v1.AccountCrede
export async function pullMyAccountInfo() {
const account = await useMasto().v1.accounts.verifyCredentials()
if (!account.acct.includes('@'))
account.acct = `${account.acct}@${currentInstance.value!.domain}`
account.acct = `${account.acct}@${currentInstance.value!.uri}`
setAccountInfo(currentUserId.value, account)
cacheAccount(account, currentServer.value, true)
@ -180,9 +180,6 @@ export function getUsersIndexByUserId(userId: string) {
}
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
if (!user.pushSubscription)
return
// clear push subscription
user.pushSubscription = undefined
const { acct } = user.account
@ -192,9 +189,12 @@ export async function removePushNotificationData(user: UserLogin, fromSWPushMana
delete useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct]
const pwaEnabled = useRuntimeConfig().public.pwaEnabled
const pwa = useNuxtApp().$pwa
const registrationError = pwa?.registrationError === true
const unregister = pwaEnabled && !registrationError && pwa?.registrationError === true && fromSWPushManager
// we remove the sw push manager if required and there are no more accounts with subscriptions
if (pwaEnabled && fromSWPushManager && (users.value.length === 0 || users.value.every(u => !u.pushSubscription))) {
if (unregister && (users.value.length === 0 || users.value.every(u => !u.pushSubscription))) {
// clear sw push subscription
try {
const registration = await navigator.serviceWorker.ready
@ -334,7 +334,7 @@ export function clearUserLocalStorage(account?: mastodon.v1.Account) {
if (!account)
return
const id = `${account.acct}@${currentInstance.value?.domain || currentServer.value}`
const id = `${account.acct}@${currentInstance.value?.uri || currentServer.value}`
// @ts-expect-error bind value to the function
;(useUserLocalStorage._ as Map<string, Ref<Record<string, any>>>).forEach((storage) => {
if (storage.value[id])

View file

@ -88,6 +88,10 @@
"not_found": "404 Not Found",
"offline_desc": "Seems like you are offline. Please check your network connection."
},
"compose": {
"draft_title": "Draft {0}",
"drafts": "Drafts ({v})"
},
"conversation": {
"with": "with"
},

View file

@ -97,6 +97,10 @@
"not_found": "404 Not Found",
"offline_desc": "Seems like you are offline. Please check your network connection."
},
"compose": {
"draft_title": "Draft {0}",
"drafts": "Drafts ({v})"
},
"conversation": {
"with": "with"
},
@ -135,12 +139,14 @@
},
"direct_message_account": "Direct message {0}",
"edit": "Edit",
"hide_reblogs": "Hide boosts from {0}",
"mention_account": "Mention {0}",
"mute_account": "Mute {0}",
"mute_conversation": "Mute this post",
"open_in_original_site": "Open in original site",
"pin_on_profile": "Pin on profile",
"share_post": "Share this post",
"show_reblogs": "Show boosts from {0}",
"show_untranslated": "Show untranslated",
"toggle_theme": {
"dark": "Toggle dark mode",

View file

@ -88,6 +88,10 @@
"not_found": "404 No Encontrado",
"offline_desc": "Al parecer estás fuera de línea. Por favor, comprueba tu conexión a la red."
},
"compose": {
"draft_title": "Borrador {0}",
"drafts": "Borradores ({v})"
},
"conversation": {
"with": "con"
},

33
modules/build-env.ts Normal file
View file

@ -0,0 +1,33 @@
import { createResolver, defineNuxtModule } from '@nuxt/kit'
import { isCI } from 'std-env'
import { getEnv, version } from '../config/env'
import type { BuildInfo } from '~/types'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtModule({
meta: {
name: 'elk:build-env',
},
async setup(_options, nuxt) {
const { env, commit, branch } = await getEnv()
const buildInfo: BuildInfo = {
version,
time: +Date.now(),
commit,
branch,
env,
}
nuxt.options.runtimeConfig.public.env = env
nuxt.options.runtimeConfig.public.buildInfo = buildInfo
nuxt.hook('nitro:config', (config) => {
config.publicAssets = config.publicAssets || []
if (env === 'dev')
config.publicAssets.push({ dir: resolve('../public-dev') })
else if (env === 'canary' || env === 'preview' || !isCI)
config.publicAssets.push({ dir: resolve('../public-staging') })
})
},
})

View file

@ -1,33 +0,0 @@
import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
import { getEnv, version } from '../config/env'
import type { BuildInfo } from '~/types'
export default defineNuxtModule({
meta: {
name: 'elk:build-info',
},
async setup(_options, nuxt) {
const { env, commit, branch } = await getEnv()
nuxt.options.runtimeConfig.public.env = env
const buildInfo: BuildInfo = {
version,
time: +Date.now(),
commit,
branch,
env,
}
addVitePlugin({
name: 'elk:build-info',
resolveId(id) {
if (id === 'virtual:build-info')
return id
},
load(id) {
if (id === 'virtual:build-info')
return `export const buildInfo = ${JSON.stringify(buildInfo, null, 2)}`
},
})
},
})

View file

@ -1,11 +1,9 @@
import { createResolver } from '@nuxt/kit'
import Inspect from 'vite-plugin-inspect'
import { isCI, isDevelopment } from 'std-env'
import { isPreview } from './config/env'
import { i18n } from './config/i18n'
import { pwa } from './config/pwa'
import { isPreview } from './config/env'
const { resolve } = createResolver(import.meta.url)
import type { BuildInfo } from './types'
export default defineNuxtConfig({
typescript: {
@ -26,7 +24,7 @@ export default defineNuxtConfig({
'@nuxtjs/color-mode',
'~/modules/purge-comments',
'~/modules/setup-components',
'~/modules/build-info',
'~/modules/build-env',
'~/modules/pwa/index', // change to '@vite-pwa/nuxt' once released and remove pwa module
'~/modules/tauri/index',
],
@ -93,7 +91,8 @@ export default defineNuxtConfig({
inviteToken: '',
},
public: {
env: '', // set in build-info module
env: '', // set in build-env module
buildInfo: {} as BuildInfo, // set in build-env module
pwaEnabled: !isDevelopment || process.env.VITE_DEV_PWA === 'true',
translateApi: '',
defaultServer: 'mas.to',
@ -112,9 +111,6 @@ export default defineNuxtConfig({
},
},
nitro: {
publicAssets: [
...(!isCI || isPreview ? [{ dir: resolve('./public-dev') }] : []),
],
prerender: {
crawlLinks: false,
routes: ['/'],

View file

@ -113,6 +113,11 @@
"vue-tsc": "^1.0.22",
"workbox-window": "^6.5.4"
},
"pnpm": {
"patchedDependencies": {
"mlly@1.0.0": "patches/mlly@1.0.0.patch"
}
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},

View file

@ -4,10 +4,9 @@ import { STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS } from '~~/constants'
const { t } = useI18n()
const masto = useMasto()
const { data, pending, error } = useLazyAsyncData(
async () => masto.v1.trends.listTags({ limit: 20 }),
{ immediate: true },
)
const paginator = masto.v1.trends.listTags({
limit: 20,
})
const hideTagsTips = useLocalStorage(STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS, false)
@ -17,28 +16,9 @@ useHeadFixed({
</script>
<template>
<CommonAlert v-if="isHydrated && !hideTagsTips && data && data.length" @close="hideTagsTips = true">
<CommonAlert v-if="!hideTagsTips" @close="hideTagsTips = true">
<p>{{ $t('tooltip.explore_tags_intro') }}</p>
</CommonAlert>
<div v-if="data && data.length">
<TagCard v-for="item of data" :key="item.name" :tag="item" border="b base" />
<div p5 text-center text-secondary-light italic>
{{ $t('common.end_of_list') }}
</div>
</div>
<div v-else-if="pending">
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op25 />
</div>
<div v-else-if="error" p5 text-center text-red italic>
{{ $t('common.error') }}: {{ error }}
</div>
<div v-else p5 text-center text-secondary italic>
{{ $t('error.explore-list-empty') }}
</div>
<TagCardPaginator v-bind="{ paginator }" />
</template>

View file

@ -2,11 +2,7 @@
const { t } = useI18n()
// limit: 20 is the default configuration of the official client
const masto = useMasto()
const { data, pending, error } = useLazyAsyncData(
async () => masto.v2.suggestions.list({ limit: 20 }),
{ immediate: true },
)
const paginator = useMasto().v2.suggestions.list({ limit: 20 })
useHeadFixed({
title: () => `${t('tab.for_you')} | ${t('nav.explore')}`,
@ -14,29 +10,19 @@ useHeadFixed({
</script>
<template>
<div v-if="data && data.length">
<AccountBigCard
v-for="suggestion of data"
:key="suggestion.account.id"
:account="suggestion.account"
as="router-link"
:to="getAccountRoute(suggestion.account)"
border="b base"
/>
<div p5 text-center text-secondary-light italic>
{{ $t('common.end_of_list') }}
</div>
</div>
<div v-else-if="pending">
<AccountBigCardSkeleton border="b base" />
<AccountBigCardSkeleton border="b base" op50 />
<AccountBigCardSkeleton border="b base" op25 />
</div>
<div v-else-if="error" p5 text-center text-red italic>
{{ $t('common.error') }}: {{ error }}
</div>
<div v-else p5 text-center text-secondary italic>
{{ $t('common.not_found') }}
</div>
<CommonPaginator :paginator="paginator" key-prop="account">
<template #default="{ item }">
<AccountBigCard
:account="item.account"
as="router-link"
:to="getAccountRoute(item.account)"
border="b base"
/>
</template>
<template #loading>
<AccountBigCardSkeleton border="b base" />
<AccountBigCardSkeleton border="b base" op50 />
<AccountBigCardSkeleton border="b base" op25 />
</template>
</CommonPaginator>
</template>

View file

@ -1,6 +1,5 @@
<script setup lang="ts">
import { buildInfo } from 'virtual:build-info'
const buildInfo = useBuildInfo()
const { t } = useI18n()
useHeadFixed({

View file

@ -1,12 +1,11 @@
<script lang="ts" setup>
import type { mastodon } from 'masto'
import { satisfies } from 'semver'
import { useForm } from 'slimeform'
import { parse } from 'ultrahtml'
definePageMeta({
middleware: 'auth',
// Keep alive the form page will reduce raw data timeliness and its status timeliness
keepalive: false,
})
const { t } = useI18n()
@ -43,21 +42,17 @@ const { form, reset, submitter, dirtyFields, isError } = useForm({
fieldsAttributes,
bot: account?.bot ?? false,
// These look more like account and privacy settings than appearance settings
// discoverable: false,
// bot: false,
// locked: false,
}
},
})
onMastoInit(async () => {
// Keep the information to be edited up to date
await pullMyAccountInfo()
reset()
})
const isCanSubmit = computed(() => !isError.value && !isEmptyObject(dirtyFields.value))
const isDirty = $computed(() => !isEmptyObject(dirtyFields.value))
const isCanSubmit = computed(() => !isError.value && isDirty)
const { submit, submitting } = submitter(async ({ dirtyFields }) => {
if (!isCanSubmit.value)
@ -76,6 +71,16 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
setAccountInfo(account!.id, res.account)
reset()
})
const refreshInfo = async () => {
// Keep the information to be edited up to date
await pullMyAccountInfo()
if (!isDirty)
reset()
}
onMastoInit(refreshInfo)
onReactivated(refreshInfo)
</script>
<template>
@ -107,15 +112,25 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
rounded-full border="bg-base 4"
w="sm:30 24" min-w="sm:30 24" h="sm:30 24"
/>
<div flex="~ col gap1" self-end>
</div>
<CommonCropImage v-model="form.avatar" />
<div px4>
<div flex justify-between>
<AccountDisplayName
:account="{ ...account, displayName: form.displayName }"
font-bold sm:text-2xl text-xl
/>
<AccountHandle :account="account" />
<label>
<AccountBotIndicator show-label px2 py1>
<template #prepend>
<input v-model="form.bot" type="checkbox">
</template>
</AccountBotIndicator>
</label>
</div>
<AccountHandle :account="account" />
</div>
<CommonCropImage v-model="form.avatar" />
</div>
<div px4 py3 space-y-5>

17
patches/mlly@1.0.0.patch Normal file
View file

@ -0,0 +1,17 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 6b5fb1566bee73cefdf165519146604b59ebe7a5..8df0f81f3df4c13bf06b003c472c46db9772db91 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -972,10 +972,10 @@ function fileURLToPath(id) {
}
const INVALID_CHAR_RE = /[\u0000-\u001F"#$&*+,/:;<=>?@[\]^`{|}\u007F]+/g;
function sanitizeURIComponent(name = "", replacement = "_") {
- return name.replace(INVALID_CHAR_RE, replacement);
+ return name.replace(INVALID_CHAR_RE, replacement).replace(/%../g, replacement);
}
function sanitizeFilePath(filePath = "") {
- return filePath.split(/[/\\]/g).map((p) => sanitizeURIComponent(p)).join("/").replace(/^([A-Za-z])_\//, "$1:/");
+ return filePath.replace(/[?#].*$/, '').split(/[/\\]/g).map((p) => sanitizeURIComponent(p)).join("/").replace(/^([A-Za-z])_\//, "$1:/");
}
function normalizeid(id) {
if (typeof id !== "string") {

View file

@ -2,30 +2,48 @@ import { useRegisterSW } from 'virtual:pwa-register/vue'
export default defineNuxtPlugin(() => {
const online = useOnline()
const registrationError = ref(false)
const swActivated = ref(false)
const registerPeriodicSync = (swUrl: string, r: ServiceWorkerRegistration) => {
setInterval(async () => {
if (!online.value)
return
const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
'cache': 'no-store',
'cache-control': 'no-cache',
},
})
if (resp?.status === 200)
await r.update()
}, 60 * 60 * 1000 /* 1 hour */)
}
const {
needRefresh, updateServiceWorker,
} = useRegisterSW({
immediate: true,
onRegisterError() {
registrationError.value = true
},
onRegisteredSW(swUrl, r) {
if (!r || r.installing)
return
setInterval(async () => {
if (!online.value)
return
const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
'cache': 'no-store',
'cache-control': 'no-cache',
},
// should add support in pwa plugin
if (r?.active?.state === 'activated') {
swActivated.value = true
registerPeriodicSync(swUrl, r)
}
else if (r?.installing) {
r.installing.addEventListener('statechange', (e) => {
const sw = e.target as ServiceWorker
swActivated.value = sw.state === 'activated'
if (swActivated.value)
registerPeriodicSync(swUrl, r)
})
if (resp?.status === 200)
await r.update()
}, 60 * 60 * 1000 /* 1 hour */)
}
},
})
@ -36,6 +54,8 @@ export default defineNuxtPlugin(() => {
return {
provide: {
pwa: reactive({
swActivated,
registrationError,
needRefresh,
updateServiceWorker,
close,

View file

@ -1,5 +1,10 @@
lockfileVersion: 5.4
patchedDependencies:
mlly@1.0.0:
hash: afe7v34zn4lohdq7767l3tlrje
path: patches/mlly@1.0.0.patch
specifiers:
'@antfu/eslint-config': ^0.34.0
'@antfu/ni': ^0.18.8
@ -1916,7 +1921,7 @@ packages:
jiti: 1.16.1
knitwork: 1.0.0
lodash.template: 4.5.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -1942,7 +1947,7 @@ packages:
jiti: 1.16.1
knitwork: 1.0.0
lodash.template: 4.5.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -2052,7 +2057,7 @@ packages:
h3: 1.0.1
knitwork: 1.0.0
magic-string: 0.26.7
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
ohash: 1.0.0
pathe: 1.0.0
perfect-debounce: 0.1.3
@ -2110,7 +2115,7 @@ packages:
js-cookie: 3.0.1
knitwork: 1.0.0
magic-string: 0.26.7
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
ufo: 1.0.1
@ -4195,7 +4200,7 @@ packages:
dotenv: 16.0.3
gittar: 0.1.1
jiti: 1.16.1
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
rc9: 2.0.0
@ -5760,7 +5765,7 @@ packages:
resolution: {integrity: sha512-MAU9ci3XdpqOX1aoIoyL2DMzW97P8LYeJxIUkfXhOfsrkH4KLHFaYDwKN0B2l6tqedVJWiTIJtWmxmZfa05vOQ==}
dependencies:
enhanced-resolve: 5.12.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
ufo: 1.0.1
dev: true
@ -7320,13 +7325,14 @@ packages:
hasBin: true
dev: true
/mlly/1.0.0:
/mlly/1.0.0_afe7v34zn4lohdq7767l3tlrje:
resolution: {integrity: sha512-QL108Hwt+u9bXdWgOI0dhzZfACovn5Aen4Xvc8Jasd9ouRH4NjnrXEiyP3nVvJo91zPlYjVRckta0Nt2zfoR6g==}
dependencies:
acorn: 8.8.1
pathe: 1.0.0
pkg-types: 1.0.1
ufo: 1.0.1
patched: true
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
@ -7421,7 +7427,7 @@ packages:
knitwork: 1.0.0
listhen: 1.0.1
mime: 3.0.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
mri: 1.2.0
node-fetch-native: 1.0.1
ofetch: 1.0.0
@ -7600,7 +7606,7 @@ packages:
hookable: 5.4.2
knitwork: 1.0.0
magic-string: 0.26.7
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
nitropack: 1.0.0
nuxi: 3.0.0
ofetch: 1.0.0
@ -7978,7 +7984,7 @@ packages:
resolution: {integrity: sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==}
dependencies:
jsonc-parser: 3.2.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
/pluralize/8.0.0:
@ -9613,7 +9619,7 @@ packages:
fast-glob: 3.2.12
local-pkg: 0.4.2
magic-string: 0.26.7
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -9630,7 +9636,7 @@ packages:
fast-glob: 3.2.12
local-pkg: 0.4.2
magic-string: 0.26.7
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -9648,7 +9654,7 @@ packages:
fast-glob: 3.2.12
local-pkg: 0.4.2
magic-string: 0.27.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -9665,7 +9671,7 @@ packages:
fast-glob: 3.2.12
local-pkg: 0.4.2
magic-string: 0.27.0
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 1.0.0
pkg-types: 1.0.1
scule: 1.0.0
@ -9924,7 +9930,7 @@ packages:
hasBin: true
dependencies:
debug: 4.3.4
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 0.2.0
source-map: 0.6.1
source-map-support: 0.5.21
@ -9945,7 +9951,7 @@ packages:
hasBin: true
dependencies:
debug: 4.3.4
mlly: 1.0.0
mlly: 1.0.0_afe7v34zn4lohdq7767l3tlrje
pathe: 0.2.0
source-map: 0.6.1
source-map-support: 0.5.21

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,11 +1,11 @@
<svg width="250" height="250" viewBox="0 0 250 250" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_13_22" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="1" width="240" height="234">
<path d="M244 123C244 187.617 205.617 235 141 235C76.3827 235 38 204.117 38 139.5C38 111.194 -8.72891 36.2356 8.00002 16C29.4601 -9.9586 88.6887 5.99994 125 5.99994C189.617 5.99994 244 58.3827 244 123Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_13_22)">
<path d="M116.94 88.0994C103.596 89.6517 96.5039 86.0813 92.2336 98.8104C92.2336 98.8104 106.57 120.465 144.774 119.922C142.639 128.77 143.63 135.29 143.63 143.129C143.63 169.208 123.041 191.95 77.6687 191.95C54.6395 191.95 26.6536 196.141 5.30196 207.861C-9.87294 216.166 -21.746 228.197 -27 244.884L-21.0444 253.345L-9.64418 253.5V301.389L-23.5532 323.355L-19.5574 387H-6.36518L-5.22134 335.773C1.33666 331.892 16.3591 321.802 29.17 306.279C46.5564 285.4 59.9011 255.052 44.1924 217.486L55.9358 212.441C68.823 243.254 64.324 269.955 53.0381 291.454C74.6185 290.756 93.1486 289.359 108.857 286.72L105.273 243.022L117.932 241.935L129.98 387H143.096L145.308 292.541C155.755 288.039 179.547 271.507 190.68 214.071C192.052 207.085 192.815 201.186 193.196 196.141C194.95 183.335 195.941 168.898 196.247 152.443L177.564 146.467H234.984L240.551 133.66C235.137 133.893 228.655 131.021 228.655 131.021L229.952 124.812H242L176.801 90.4279C169.557 93.222 161.931 96.87 156.593 101.294C152.323 98.1895 137.53 88.4874 116.94 88.0994Z" fill="#5AB1CC"/>
<path d="M6.21704 24.4927L18.4942 21C24.4422 42.5773 31.839 54.375 41.1422 60.3515C49.5303 65.4509 60.8925 65.5906 72.9409 64.9309C69.4331 63.7666 66.1541 62.1367 63.1039 59.8858C56.3171 54.8407 50.5217 46.4582 46.1751 31.2454L58.376 27.5974C61.655 39.0846 65.4678 45.682 70.577 49.4852C75.6861 53.2108 81.8628 54.1422 89.1834 54.9184C102.909 56.4707 120.067 57.0916 141.495 67.1817C144.393 68.2684 147.367 69.6655 150.264 71.2178C149.883 70.4416 149.502 69.6655 148.968 68.8117C145.308 62.9904 138.14 56.8588 124.871 51.8913L129.141 39.7832C150.722 47.7 159.262 58.9544 162.694 67.8803C166.659 78.048 164.219 86.0037 164.219 86.0037C161.169 87.0127 158.119 88.4098 154.611 89.4964C147.977 84.9171 141.724 81.4631 135.776 78.7466C113.814 70.4416 92.3099 76.1076 73.2459 77.8928C58.9098 79.2123 45.7938 78.5913 34.4317 71.2954C23.2221 64.1547 13.3851 50.4942 6.21704 24.4927Z" fill="#1E4E5E"/>
<path d="M90.0984 45.2939C87.582 39.5503 86.0569 32.4872 86.7432 23.7942L99.4016 24.7256C98.6391 35.2814 102.299 42.4221 106.417 47.0791C101.079 46.1477 95.9701 46.039 90.0984 45.2939Z" fill="#1E4E5E"/>
<path d="M170.167 43.9744L178.479 34.2724C200.059 53.366 186.638 80.687 186.638 80.687L174.819 79.3675C174.437 73.1271 173.675 61.5313 168.184 54.996C171.768 56.8355 174.819 58.8613 178.174 61.9038C178.174 56.2378 176.42 49.5628 170.167 43.9744Z" fill="#1E4E5E"/>
</g>
<mask id="mask0_107_22" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="1" width="240" height="234">
<path d="M244 123C244 187.617 205.617 235 141 235C76.3827 235 38 204.117 38 139.5C38 111.194 -8.72891 36.2357 8.00002 16C29.4601 -9.9586 88.6887 5.99994 125 5.99994C189.617 5.99994 244 58.3827 244 123Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_107_22)">
<path d="M116.94 88.0994C103.596 89.6517 96.5039 86.0813 92.2336 98.8104C92.2336 98.8104 106.57 120.465 144.774 119.922C142.639 128.77 143.63 135.29 143.63 143.129C143.63 169.208 123.041 191.95 77.6687 191.95C54.6395 191.95 26.6536 196.141 5.30196 207.861C-9.87294 216.166 -21.746 228.197 -27 244.884L-21.0444 253.345L-9.64418 253.5V301.389L-23.5532 323.355L-19.5574 387H-6.36518L-5.22134 335.773C1.33666 331.892 16.3591 321.802 29.17 306.279C46.5564 285.4 59.9011 255.052 44.1924 217.486L55.9358 212.441C68.823 243.254 64.324 269.955 53.0381 291.454C74.6185 290.756 93.1486 289.359 108.857 286.72L105.273 243.022L117.932 241.935L129.98 387H143.096L145.308 292.541C155.755 288.039 179.547 271.507 190.68 214.071C192.052 207.085 192.815 201.186 193.196 196.141C194.95 183.335 195.941 168.898 196.247 152.443L177.564 146.467H234.984L240.551 133.66C235.137 133.893 228.655 131.021 228.655 131.021L229.952 124.812H242L176.801 90.4279C169.557 93.222 161.931 96.87 156.593 101.294C152.323 98.1895 137.53 88.4874 116.94 88.0994Z" fill="#91BA4D"/>
<path d="M6.21704 24.4927L18.4942 21C24.4422 42.5773 31.839 54.375 41.1422 60.3515C49.5303 65.4509 60.8925 65.5906 72.9409 64.9309C69.4331 63.7666 66.1541 62.1367 63.1039 59.8858C56.3171 54.8407 50.5217 46.4582 46.1751 31.2454L58.376 27.5974C61.655 39.0846 65.4678 45.682 70.577 49.4852C75.6861 53.2108 81.8628 54.1422 89.1834 54.9184C102.909 56.4707 120.067 57.0916 141.495 67.1817C144.393 68.2684 147.367 69.6655 150.264 71.2178C149.883 70.4416 149.502 69.6655 148.968 68.8117C145.308 62.9904 138.14 56.8588 124.871 51.8913L129.141 39.7832C150.722 47.7 159.262 58.9544 162.694 67.8803C166.659 78.048 164.219 86.0037 164.219 86.0037C161.169 87.0127 158.119 88.4098 154.611 89.4964C147.977 84.9171 141.724 81.4631 135.776 78.7466C113.814 70.4416 92.3099 76.1076 73.2459 77.8928C58.9098 79.2123 45.7938 78.5913 34.4317 71.2954C23.2221 64.1547 13.3851 50.4942 6.21704 24.4927Z" fill="#20461A"/>
<path d="M90.0984 45.2939C87.582 39.5503 86.0569 32.4872 86.7432 23.7942L99.4016 24.7256C98.6391 35.2814 102.299 42.4221 106.417 47.0791C101.079 46.1477 95.9701 46.039 90.0984 45.2939Z" fill="#20461A"/>
<path d="M170.167 43.9744L178.479 34.2724C200.059 53.366 186.638 80.687 186.638 80.687L174.819 79.3675C174.437 73.1271 173.675 61.5313 168.184 54.996C171.768 56.8355 174.819 58.8613 178.174 61.9038C178.174 56.2378 176.42 49.5628 170.167 43.9744Z" fill="#20461A"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public-staging/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

11
public-staging/logo.svg Normal file
View file

@ -0,0 +1,11 @@
<svg width="250" height="250" viewBox="0 0 250 250" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_13_22" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="1" width="240" height="234">
<path d="M244 123C244 187.617 205.617 235 141 235C76.3827 235 38 204.117 38 139.5C38 111.194 -8.72891 36.2356 8.00002 16C29.4601 -9.9586 88.6887 5.99994 125 5.99994C189.617 5.99994 244 58.3827 244 123Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_13_22)">
<path d="M116.94 88.0994C103.596 89.6517 96.5039 86.0813 92.2336 98.8104C92.2336 98.8104 106.57 120.465 144.774 119.922C142.639 128.77 143.63 135.29 143.63 143.129C143.63 169.208 123.041 191.95 77.6687 191.95C54.6395 191.95 26.6536 196.141 5.30196 207.861C-9.87294 216.166 -21.746 228.197 -27 244.884L-21.0444 253.345L-9.64418 253.5V301.389L-23.5532 323.355L-19.5574 387H-6.36518L-5.22134 335.773C1.33666 331.892 16.3591 321.802 29.17 306.279C46.5564 285.4 59.9011 255.052 44.1924 217.486L55.9358 212.441C68.823 243.254 64.324 269.955 53.0381 291.454C74.6185 290.756 93.1486 289.359 108.857 286.72L105.273 243.022L117.932 241.935L129.98 387H143.096L145.308 292.541C155.755 288.039 179.547 271.507 190.68 214.071C192.052 207.085 192.815 201.186 193.196 196.141C194.95 183.335 195.941 168.898 196.247 152.443L177.564 146.467H234.984L240.551 133.66C235.137 133.893 228.655 131.021 228.655 131.021L229.952 124.812H242L176.801 90.4279C169.557 93.222 161.931 96.87 156.593 101.294C152.323 98.1895 137.53 88.4874 116.94 88.0994Z" fill="#5AB1CC"/>
<path d="M6.21704 24.4927L18.4942 21C24.4422 42.5773 31.839 54.375 41.1422 60.3515C49.5303 65.4509 60.8925 65.5906 72.9409 64.9309C69.4331 63.7666 66.1541 62.1367 63.1039 59.8858C56.3171 54.8407 50.5217 46.4582 46.1751 31.2454L58.376 27.5974C61.655 39.0846 65.4678 45.682 70.577 49.4852C75.6861 53.2108 81.8628 54.1422 89.1834 54.9184C102.909 56.4707 120.067 57.0916 141.495 67.1817C144.393 68.2684 147.367 69.6655 150.264 71.2178C149.883 70.4416 149.502 69.6655 148.968 68.8117C145.308 62.9904 138.14 56.8588 124.871 51.8913L129.141 39.7832C150.722 47.7 159.262 58.9544 162.694 67.8803C166.659 78.048 164.219 86.0037 164.219 86.0037C161.169 87.0127 158.119 88.4098 154.611 89.4964C147.977 84.9171 141.724 81.4631 135.776 78.7466C113.814 70.4416 92.3099 76.1076 73.2459 77.8928C58.9098 79.2123 45.7938 78.5913 34.4317 71.2954C23.2221 64.1547 13.3851 50.4942 6.21704 24.4927Z" fill="#1E4E5E"/>
<path d="M90.0984 45.2939C87.582 39.5503 86.0569 32.4872 86.7432 23.7942L99.4016 24.7256C98.6391 35.2814 102.299 42.4221 106.417 47.0791C101.079 46.1477 95.9701 46.039 90.0984 45.2939Z" fill="#1E4E5E"/>
<path d="M170.167 43.9744L178.479 34.2724C200.059 53.366 186.638 80.687 186.638 80.687L174.819 79.3675C174.437 73.1271 173.675 61.5313 168.184 54.996C171.768 56.8355 174.819 58.8613 178.174 61.9038C178.174 56.2378 176.42 49.5628 170.167 43.9744Z" fill="#1E4E5E"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

5
shims.d.ts vendored
View file

@ -1,8 +1,3 @@
/// <reference types="@types/wicg-file-system-access" />
/// <reference types="vite-plugin-pwa/info" />
/// <reference types="vite-plugin-pwa/client" />
declare module 'virtual:build-info' {
import type { BuildInfo } from '~/types'
export const buildInfo: BuildInfo
}

View file

@ -27,12 +27,6 @@ export interface ElkMasto extends mastodon.Client {
export type PaginatorState = 'idle' | 'loading' | 'done' | 'error'
export interface ServerInfo extends mastodon.v2.Instance {
server: string
timeUpdated: number
customEmojis?: Record<string, mastodon.v1.CustomEmoji>
}
export interface GroupedNotifications {
id: string
type: Exclude<string, 'grouped-reblogs-and-favourites'>