Re-implement basic account profile

This commit is contained in:
AkiraFukushima 2023-01-23 01:47:19 +09:00
parent 2a2ebfd0ae
commit 1724c808bf
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
1 changed files with 518 additions and 4 deletions

View File

@ -1,12 +1,526 @@
<template></template>
<template>
<div id="account_profile" role="article" aria-label="account profile" v-if="user">
<div class="header-background" v-bind:style="{ backgroundImage: 'url(' + user.header + ')' }">
<div class="header">
<div class="relationship" v-if="relationship !== null && !isOwnProfile">
<div class="follower-status">
<el-tag class="status" size="small" v-if="relationship.followed_by">{{ $t('side_bar.account_profile.follows_you') }}</el-tag>
<el-tag class="status" size="default" v-else>{{ $t('side_bar.account_profile.doesnt_follow_you') }}</el-tag>
</div>
<div class="notify" v-if="relationship !== null && !isOwnProfile">
<div
v-if="relationship.notifying"
class="unsubscribe"
@click="unsubscribe(user)"
:title="$t('side_bar.account_profile.unsubscribe')"
>
<font-awesome-icon icon="bell" size="lg" />
</div>
<div v-else class="subscribe" @click="subscribe(user)" :title="$t('side_bar.account_profile.subscribe')">
<font-awesome-icon :icon="['far', 'bell']" size="lg" />
</div>
</div>
</div>
<div class="user-info">
<div class="more" v-if="relationship !== null && !isOwnProfile">
<el-popover placement="bottom" width="200" trigger="click" popper-class="account-menu-popper">
<ul class="menu-list">
<li role="button" @click="openBrowser(user!.url)">
{{ $t('side_bar.account_profile.open_in_browser') }}
</li>
<li role="button" @click="addToList(user)">
{{ $t('side_bar.account_profile.manage_list_memberships') }}
</li>
<li role="button" @click="unmute(user)" v-if="relationship.muting">
{{ $t('side_bar.account_profile.unmute') }}
</li>
<li role="button" @click="confirmMute(user)" v-else>
{{ $t('side_bar.account_profile.mute') }}
</li>
<li role="button" @click="unblock(user)" v-if="relationship.blocking">
{{ $t('side_bar.account_profile.unblock') }}
</li>
<li role="button" @click="block(user)" v-else>
{{ $t('side_bar.account_profile.block') }}
</li>
</ul>
<template #reference>
<el-button link :title="$t('side_bar.account_profile.detail')">
<font-awesome-icon icon="gear" size="xl" />
</el-button>
</template>
</el-popover>
</div>
<div class="icon" role="presentation">
<FailoverImg :src="user.avatar" :alt="`Avatar of ${user.username}`" />
</div>
<div class="follow-status" v-if="relationship !== null && !isOwnProfile">
<div v-if="relationship.following" class="unfollow" @click="unfollow(user)" :title="$t('side_bar.account_profile.unfollow')">
<font-awesome-icon icon="user-xmark" size="xl" />
</div>
<div v-else-if="relationship.requested" :title="$t('side_bar.account_profile.follow_requested')">
<font-awesome-icon icon="hourglass" size="xl" />
</div>
<div v-else class="follow" @click="follow(user)" :title="$t('side_bar.account_profile.follow')">
<font-awesome-icon icon="user-plus" size="xl" />
</div>
</div>
</div>
<div class="username">
<bdi v-html="username(user)"></bdi>
</div>
<div class="account">@{{ user.acct }}</div>
<div class="note" v-html="note(user)" @click.capture.prevent="noteClick"></div>
</div>
</div>
<div class="identity">
<dl v-for="(identity, index) in identityProofs" :key="index">
<dt>
{{ identity.provider }}
</dt>
<dd @click.capture.prevent="openBrowser(identity.profile_url)">
{{ identity.provider_username }}
</dd>
</dl>
</div>
<div class="metadata">
<dl v-for="(data, index) in user.fields" :key="index">
<dt>
{{ data.name }}
</dt>
<dd v-html="data.value" @click.capture.prevent="metadataClick"></dd>
</dl>
</div>
<div class="timeline"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent, computed, ref, reactive, onMounted, watch } from 'vue'
import { useI18next } from 'vue3-i18next'
import { useRoute } from 'vue-router'
import generator, { Entity, MegalodonInterface } from 'megalodon'
import { useStore } from '@/store'
import emojify from '@/utils/emojify'
import { findLink } from '@/utils/tootParser'
import { MyWindow } from '~/src/types/global'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { ACTION_TYPES as LIST_MEMBERSHIP_ACTION } from '@/store/TimelineSpace/Modals/ListMembership'
import { ACTION_TYPES as MUTE_ACTION } from '@/store/TimelineSpace/Modals/MuteConfirm'
import FailoverImg from '@/components/atoms/FailoverImg.vue'
export default defineComponent({
name: 'Profile',
setup() {}
components: {
FailoverImg
},
setup() {
const win = (window as any) as MyWindow
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const theme = computed(() => {
return {
'--theme-mask-color': store.state.App.theme.wrapper_mask_color,
'--theme-border-color': store.state.App.theme.border_color,
'--theme-primary-color': store.state.App.theme.primary_color
}
})
const client = ref<MegalodonInterface | null>(null)
const id = computed(() => parseInt(route.params.id as string))
const userId = computed(() => route.query.account_id?.toString())
const userAgent = computed(() => store.state.App.userAgent)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const user = ref<Entity.Account | null>(null)
const relationship = ref<Entity.Relationship | null>(null)
const isOwnProfile = computed(() => {
if (!account.account || !account.server || !user.value) return false
// For Mastodon
if (`${account.server?.baseURL}/@${account.account?.username}` === user.value.url) return true
// For Pleroma
if (`${account.server.baseURL}/users/${account.account.username}` === user.value.url) return true
return false
})
const identityProofs = ref<Array<Entity.IdentityProof>>([])
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
const c = generator(s.sns, s.baseURL, a.accessToken, userAgent.value)
client.value = c
if (userId.value) {
await load(userId.value)
}
})
watch(userId, async current => {
if (current) {
await load(current)
}
})
const load = async (id: string) => {
if (client.value) {
const res = await client.value.getAccount(id)
user.value = res.data
const rel = await client.value.getRelationship(id)
relationship.value = rel.data
const proofs = await client.value.getIdentityProof(id)
identityProofs.value = proofs.data
}
}
const unsubscribe = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.unsubscribeAccount(a.id)
relationship.value = res.data
}
}
const subscribe = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.subscribeAccount(a.id)
relationship.value = res.data
}
}
const openBrowser = (text: string) => {
win.ipcRenderer.invoke('open-browser', text)
}
const addToList = (a: Entity.Account | null) => {
if (a) {
store.dispatch(`TimelineSpace/Modals/ListMembership/${LIST_MEMBERSHIP_ACTION.SET_ACCOUNT}`, a)
store.dispatch(`TimelineSpace/Modals/ListMembership/${LIST_MEMBERSHIP_ACTION.CHANGE_MODAL}`, true)
}
}
const unmute = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.unmuteAccount(a.id)
relationship.value = res.data
}
}
const confirmMute = (a: Entity.Account | null) => {
if (a) {
store.dispatch(`TimelineSpace/Modals/MuteConfirm/${MUTE_ACTION.CHANGE_ACCOUNT}`, a)
store.dispatch(`TimelineSpace/Modals/MuteConfirm/${MUTE_ACTION.CHANGE_MODAL}`, true)
}
}
const unblock = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.unblockAccount(a.id)
relationship.value = res.data
}
}
const block = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.blockAccount(a.id)
relationship.value = res.data
}
}
const unfollow = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.unfollowAccount(a.id)
relationship.value = res.data
}
}
const follow = async (a: Entity.Account | null) => {
if (client.value && a) {
const res = await client.value.followAccount(a.id)
relationship.value = res.data
}
}
const username = (a: Entity.Account | null) => {
if (!a) return ''
if (a.display_name !== '') {
return emojify(a.display_name, a.emojis)
} else {
return a.username
}
}
const note = (a: Entity.Account | null) => {
if (!a) return ''
return emojify(a.note, a.emojis)
}
const noteClick = (e: Event) => {
const link = findLink(e.target as HTMLElement, 'note')
if (link !== null) {
win.ipcRenderer.invoke('open-browser', link)
}
}
const metadataClick = (e: Event) => {
const link = findLink(e.target as HTMLElement, 'metadata')
if (link !== null) {
win.ipcRenderer.invoke('open-browser', link)
}
}
return {
user,
relationship,
theme,
isOwnProfile,
unsubscribe,
subscribe,
openBrowser,
addToList,
unmute,
confirmMute,
unblock,
block,
unfollow,
follow,
username,
note,
noteClick,
identityProofs,
metadataClick
}
}
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
#account_profile {
.header-background {
background-position: 50% 50%;
background-size: cover;
}
.header {
background-color: var(--theme-wrapper-mask-color);
text-align: center;
padding: 12px;
box-sizing: border-box;
word-wrap: break-word;
font-size: var(--base-font-size);
.relationship {
display: flex;
justify-content: space-between;
.follower-status {
.status {
color: #fff;
background-color: rgba(0, 0, 0, 0.3);
font-size: 1rem;
}
}
.notify {
cursor: pointer;
.unsubscribe {
color: #409eff;
}
}
}
.user-info {
display: flex;
justify-content: space-around;
align-items: center;
.follow-status {
.follow {
cursor: pointer;
}
.unfollow {
color: #409eff;
cursor: pointer;
}
}
.icon {
padding: 12px;
img {
width: 72px;
border-radius: 8px;
}
}
}
.username {
overflow: hidden;
text-overflow: ellipsis;
font-size: calc(var(--base-font-size) * 1.71);
margin: 0 auto 12px auto;
}
.username :deep(.emojione) {
max-width: 1em;
max-height: 1em;
}
.account {
color: #409eff;
}
.note :deep(.emojione) {
max-width: 1.2em;
height: 1.2em;
}
}
.identity {
dl {
display: flex;
border-top: 1px solid var(--theme-border-color);
margin: 0;
dt {
background-color: var(--theme-selected-background-color);
flex: 0 0 auto;
width: 120px;
text-align: center;
padding: 16px 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dd {
background-color: rgba(121, 189, 154, 0.25);
flex: 1 1 auto;
padding: 16px 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
margin: 0;
color: #79bd9a;
font-weight: bold;
cursor: pointer;
}
}
}
.metadata {
dl {
display: flex;
border-top: 1px solid var(--theme-border-color);
margin: 0;
dt {
background-color: var(--theme-selected-background-color);
flex: 0 0 auto;
width: 120px;
text-align: center;
padding: 16px 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dd {
flex: 1 1 auto;
padding: 16px 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
margin: 0;
}
}
}
.basic-info {
.info {
border-top: solid 1px var(--theme-border-color);
border-bottom: solid 1px var(--theme-border-color);
border-left: solid 1px var(--theme-border-color);
padding: 0 4px;
.tab {
margin: 0;
padding: 0;
width: 100%;
text-align: left;
line-height: 20px;
height: auto;
display: block;
}
.tab :deep(span) {
display: block;
}
.title {
font-size: calc(var(--base-font-size) * 0.8);
color: #909399;
}
.count {
font-weight: 800;
font-size: calc(var(--base-font-size) * 1.14);
color: var(--theme-primary-color);
}
}
.info-active {
border-bottom: solid 1px #409eff;
.count {
color: #409eff;
}
}
.info:first-of-type {
border-left: none;
}
}
.timeline {
font-size: calc(var(--base-font-size) * 0.85);
}
}
</style>
<style lang="scss">
.account-menu-popper {
padding: 2px 0 !important;
border-color: #909399;
.menu-list {
padding: 0;
font-size: calc(var(--base-font-size) * 0.9);
list-style-type: none;
line-height: 32px;
text-align: left;
color: #303133;
margin: 4px 0;
li {
box-sizing: border-box;
padding: 0 32px 0 16px;
&:hover {
background-color: #409eff;
color: #fff;
cursor: pointer;
}
}
}
}
</style>