Re-implement basic account profile
This commit is contained in:
parent
2a2ebfd0ae
commit
1724c808bf
|
@ -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">
|
<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({
|
export default defineComponent({
|
||||||
name: 'Profile',
|
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>
|
</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>
|
||||||
|
|
Loading…
Reference in New Issue