forked from Mirrors/elk
274 lines
10 KiB
Vue
274 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import type { mastodon } from 'masto'
|
|
|
|
const { account } = defineProps<{
|
|
account: mastodon.v1.Account
|
|
command?: boolean
|
|
}>()
|
|
|
|
const { client } = useMasto()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const createdAt = useFormattedDateTime(() => account.createdAt, {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})
|
|
|
|
const relationship = useRelationship(account)
|
|
|
|
const namedFields = ref<mastodon.v1.AccountField[]>([])
|
|
const iconFields = ref<mastodon.v1.AccountField[]>([])
|
|
const isEditingPersonalNote = ref<boolean>(false)
|
|
const hasHeader = computed(() => !account.header.endsWith('/original/missing.png'))
|
|
const isCopied = ref<boolean>(false)
|
|
|
|
function getFieldIconTitle(fieldName: string) {
|
|
return fieldName === 'Joined' ? t('account.joined') : fieldName
|
|
}
|
|
|
|
function getNotificationIconTitle() {
|
|
return relationship.value?.notifying ? t('account.notifications_on_post_disable', { username: `@${account.username}` }) : t('account.notifications_on_post_enable', { username: `@${account.username}` })
|
|
}
|
|
|
|
function previewHeader() {
|
|
openMediaPreview([{
|
|
id: `${account.acct}:header`,
|
|
type: 'image',
|
|
previewUrl: account.header,
|
|
description: t('account.profile_description', [account.username]),
|
|
}])
|
|
}
|
|
|
|
function previewAvatar() {
|
|
openMediaPreview([{
|
|
id: `${account.acct}:avatar`,
|
|
type: 'image',
|
|
previewUrl: account.avatar,
|
|
description: t('account.avatar_description', [account.username]),
|
|
}])
|
|
}
|
|
|
|
async function toggleNotifications() {
|
|
relationship.value!.notifying = !relationship.value?.notifying
|
|
try {
|
|
const newRel = await client.value.v1.accounts.$select(account.id).follow({ notify: relationship.value?.notifying })
|
|
Object.assign(relationship!, newRel)
|
|
}
|
|
catch {
|
|
// TODO error handling
|
|
relationship.value!.notifying = !relationship.value?.notifying
|
|
}
|
|
}
|
|
|
|
watchEffect(() => {
|
|
const named: mastodon.v1.AccountField[] = []
|
|
const icons: mastodon.v1.AccountField[] = []
|
|
|
|
account.fields?.forEach((field) => {
|
|
const icon = getAccountFieldIcon(field.name)
|
|
if (icon)
|
|
icons.push(field)
|
|
else
|
|
named.push(field)
|
|
})
|
|
icons.push({
|
|
name: 'Joined',
|
|
value: createdAt.value,
|
|
})
|
|
|
|
namedFields.value = named
|
|
iconFields.value = icons
|
|
})
|
|
|
|
const personalNoteDraft = ref(relationship.value?.note ?? '')
|
|
watch(relationship, (relationship, oldValue) => {
|
|
if (!oldValue && relationship)
|
|
personalNoteDraft.value = relationship.note ?? ''
|
|
})
|
|
|
|
async function editNote(event: Event) {
|
|
if (!event.target || !('value' in event.target) || !relationship.value)
|
|
return
|
|
|
|
const newNote = event.target?.value as string
|
|
|
|
if (relationship.value.note?.trim() === newNote.trim())
|
|
return
|
|
|
|
const newNoteApiResult = await client.value.v1.accounts.$select(account.id).note.create({ comment: newNote })
|
|
relationship.value.note = newNoteApiResult.note
|
|
personalNoteDraft.value = relationship.value.note ?? ''
|
|
}
|
|
|
|
const isSelf = useSelfAccount(() => account)
|
|
const isNotifiedOnPost = computed(() => !!relationship.value?.notifying)
|
|
|
|
const personalNoteMaxLength = 2000
|
|
|
|
async function copyAccountName() {
|
|
try {
|
|
const shortHandle = getShortHandle(account)
|
|
const serverName = getServerName(account)
|
|
const accountName = `${shortHandle}@${serverName}`
|
|
await navigator.clipboard.writeText(accountName)
|
|
}
|
|
catch (err) {
|
|
console.error('Failed to copy account name:', err)
|
|
}
|
|
|
|
isCopied.value = true
|
|
setTimeout(() => {
|
|
isCopied.value = false
|
|
}, 2000)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div flex flex-col>
|
|
<div v-if="relationship?.requestedBy" p-4 flex justify-between items-center bg-card>
|
|
<span text-primary font-bold>{{ $t('account.requested', [account.displayName]) }}</span>
|
|
<AccountFollowRequestButton :account="account" :relationship="relationship" />
|
|
</div>
|
|
<component :is="hasHeader ? 'button' : 'div'" border="b base" z-1 @click="hasHeader ? previewHeader() : undefined">
|
|
<img h-50 height="200" w-full object-cover :src="account.header" :alt="t('account.profile_description', [account.username])">
|
|
</component>
|
|
<div p4 mt--18 flex flex-col gap-4>
|
|
<div relative>
|
|
<div flex justify-between>
|
|
<button shrink-0 h-full :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" p1 bg-base border-bg-base z-2 @click="previewAvatar">
|
|
<AccountAvatar :square="isSelf" :account="account" hover:opacity-90 transition-opacity w-28 h-28 />
|
|
</button>
|
|
<div inset-ie-0 flex="~ wrap row-reverse" gap-2 items-center pt18 justify-start>
|
|
<!-- Edit profile -->
|
|
<NuxtLink
|
|
v-if="isSelf"
|
|
to="/settings/profile/appearance"
|
|
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
|
|
hover="border-primary text-primary bg-active"
|
|
>
|
|
{{ $t('settings.profile.appearance.title') }}
|
|
</NuxtLink>
|
|
<AccountFollowButton :account="account" :command="command" />
|
|
<span inset-ie-0 flex gap-2 items-center>
|
|
<AccountMoreButton
|
|
:account="account" :command="command"
|
|
@add-note="isEditingPersonalNote = true"
|
|
@remove-note="() => { isEditingPersonalNote = false; personalNoteDraft = '' }"
|
|
/>
|
|
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
|
|
<button
|
|
:aria-pressed="isNotifiedOnPost"
|
|
:aria-label="t('account.notifications_on_post_enable', { username: `@${account.username}` })"
|
|
rounded-full text-sm p2 border-1 transition-colors
|
|
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
|
|
@click="toggleNotifications"
|
|
>
|
|
<span v-if="isNotifiedOnPost" i-ri:notification-4-fill block text-current />
|
|
<span v-else i-ri-notification-4-line block text-current />
|
|
</button>
|
|
</CommonTooltip>
|
|
<CommonTooltip :content="$t('list.modify_account')">
|
|
<VDropdown v-if="!isSelf && relationship?.following">
|
|
<button
|
|
:aria-label="$t('list.modify_account')"
|
|
rounded-full text-sm p2 border-1 transition-colors
|
|
border-base hover:text-primary
|
|
>
|
|
<span i-ri:play-list-add-fill block text-current />
|
|
</button>
|
|
<template #popper>
|
|
<ListLists :user-id="account.id" />
|
|
</template>
|
|
</VDropdown>
|
|
</CommonTooltip>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div flex="~ col gap1" pt2>
|
|
<div flex gap2 items-center flex-wrap>
|
|
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
|
|
<AccountRolesIndicator v-if="account.roles?.length" :account="account" />
|
|
<AccountLockIndicator v-if="account.locked" show-label />
|
|
<AccountBotIndicator v-if="account.bot" show-label />
|
|
</div>
|
|
|
|
<div flex items-center gap-1>
|
|
<AccountHandle :account="account" overflow-unset line-clamp-unset />
|
|
<CommonTooltip placement="bottom" :content="$t('account.copy_account_name')" no-auto-focus flex>
|
|
<button text-secondary-light text-sm :class="isCopied ? 'i-ri:check-fill text-green' : 'i-ri:file-copy-line'" @click="copyAccountName">
|
|
<span sr-only>{{ $t('account.copy_account_name') }}</span>
|
|
</button>
|
|
</CommonTooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<label
|
|
v-if="isEditingPersonalNote || (relationship?.note && relationship.note.length > 0)"
|
|
space-y-2
|
|
pb-4
|
|
block
|
|
border="b base"
|
|
>
|
|
<div flex flex-row space-x-2 flex-v-center>
|
|
<div i-ri-edit-2-line />
|
|
<p font-medium>
|
|
{{ $t('account.profile_personal_note') }}
|
|
</p>
|
|
<p text-secondary text-sm :class="{ 'text-orange': personalNoteDraft.length > (personalNoteMaxLength - 100) }">
|
|
{{ personalNoteDraft.length }} / {{ personalNoteMaxLength }}
|
|
</p>
|
|
</div>
|
|
<div position-relative>
|
|
<div
|
|
input-base
|
|
min-h-10ex
|
|
whitespace-pre-wrap
|
|
opacity-0
|
|
:class="{ 'trailing-newline': personalNoteDraft.endsWith('\n') }"
|
|
>
|
|
{{ personalNoteDraft }}
|
|
</div>
|
|
<textarea
|
|
v-model="personalNoteDraft"
|
|
input-base
|
|
position-absolute
|
|
style="height: 100%"
|
|
top-0
|
|
resize-none
|
|
:maxlength="personalNoteMaxLength"
|
|
@change="editNote"
|
|
/>
|
|
</div>
|
|
</label>
|
|
<div v-if="account.note" max-h-100 overflow-y-auto>
|
|
<ContentRich text-4 text-base :content="account.note" :emojis="account.emojis" />
|
|
</div>
|
|
<div v-if="namedFields.length" flex="~ col wrap gap1">
|
|
<div v-for="field in namedFields" :key="field.name" flex="~ gap-1" items-center>
|
|
<div mt="0.5" text-secondary uppercase text-xs font-bold>
|
|
<ContentRich :content="field.name" :emojis="account.emojis" />
|
|
</div>
|
|
<span text-secondary text-xs font-bold>|</span>
|
|
<ContentRich :content="field.value" :emojis="account.emojis" />
|
|
</div>
|
|
</div>
|
|
<div v-if="iconFields.length" flex="~ wrap gap-2">
|
|
<div v-for="field in iconFields" :key="field.name" flex="~ gap-1" px1 items-center :class="`${field.verifiedAt ? 'border-1 rounded-full border-dark' : ''}`">
|
|
<CommonTooltip :content="getFieldIconTitle(field.name)">
|
|
<div text-secondary :class="getAccountFieldIcon(field.name)" :title="getFieldIconTitle(field.name)" />
|
|
</CommonTooltip>
|
|
<ContentRich text-sm :content="field.value" :emojis="account.emojis" />
|
|
</div>
|
|
</div>
|
|
<AccountPostsFollowers :account="account" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.trailing-newline::after {
|
|
content: '\a';
|
|
}
|
|
</style>
|