Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85650421
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 65140d4e39..6787930724 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,204 +1,208 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import UserNote from '../user_note/user_note.vue'
import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBell,
faRss,
faSearchPlus,
faExternalLinkAlt,
faEdit,
faTimes,
faExpandAlt
} from '@fortawesome/free-solid-svg-icons'
library.add(
faRss,
faBell,
faSearchPlus,
faExternalLinkAlt,
faEdit,
faTimes,
faExpandAlt
)
export default {
props: [
'userId',
'switcher',
'selected',
'hideBio',
'rounded',
'bordered',
'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
- 'onClose'
+ 'onClose',
+ 'hasNoteEditor'
],
data () {
return {
followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
created () {
this.$store.dispatch('fetchUserRelationship', this.user.id)
},
computed: {
user () {
return this.$store.getters.findUser(this.userId)
},
relationship () {
return this.$store.getters.relationship(this.userId)
},
classes () {
return [{
'-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
'-rounded': this.rounded === true, // set border-radius for all sides
'-bordered': this.bordered === true, // set border for all sides
'-popover': !!this.onClose // set popover rounding
}]
},
style () {
return {
backgroundImage: [
'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
`url(${this.user.cover_photo})`
].join(', ')
}
},
isOtherUser () {
return this.user.id !== this.$store.state.users.currentUser.id
},
subscribeUrl () {
// eslint-disable-next-line no-undef
const serverUrl = new URL(this.user.statusnet_profile_url)
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
},
loggedIn () {
return this.$store.state.users.currentUser
},
dailyAvg () {
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
return Math.round(this.user.statuses_count / days)
},
userHighlightType: {
get () {
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return (data && data.type) || 'disabled'
},
set (type) {
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
if (type !== 'disabled') {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type })
} else {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
}
},
...mapGetters(['mergedConfig'])
},
userHighlightColor: {
get () {
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return data && data.color
},
set (color) {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
}
},
visibleRole () {
const rights = this.user.rights
if (!rights) { return }
const validRole = rights.admin || rights.moderator
const roleTitle = rights.admin ? 'admin' : 'moderator'
return validRole && roleTitle
},
hideFollowsCount () {
return this.isOtherUser && this.user.hide_follows_count
},
hideFollowersCount () {
return this.isOtherUser && this.user.hide_followers_count
},
showModerationMenu () {
const privileges = this.loggedIn.privileges
return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags')
},
+ hasNote () {
+ return this.relationship.note
+ },
supportsNote () {
return 'note' in this.relationship
},
...mapGetters(['mergedConfig'])
},
components: {
UserAvatar,
RemoteFollow,
ModerationTools,
AccountActions,
ProgressButton,
FollowButton,
Select,
RichContent,
UserLink,
UserNote
},
methods: {
muteUser () {
this.$store.dispatch('muteUser', this.user.id)
},
unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id)
},
subscribeUser () {
return this.$store.dispatch('subscribeUser', this.user.id)
},
unsubscribeUser () {
return this.$store.dispatch('unsubscribeUser', this.user.id)
},
setProfileView (v) {
if (this.switcher) {
const store = this.$store
store.commit('setProfileView', { v })
}
},
linkClicked ({ target }) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
userProfileLink (user) {
return generateProfileLink(
user.id, user.screen_name,
this.$store.state.instance.restrictedNicknames
)
},
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
zoomAvatar () {
const attachment = {
url: this.user.profile_image_url_original,
mimetype: 'image'
}
this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrentMedia', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
},
onAvatarClickHandler (e) {
if (this.onAvatarClick) {
e.preventDefault()
this.onAvatarClick()
}
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 88434c59ec..349c7cb160 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,322 +1,322 @@
<template>
<div
class="user-card"
:class="classes"
>
<div
:class="{ 'hide-bio': hideBio }"
:style="style"
class="background-image"
/>
<div :class="onClose ? '' : panel-heading -flexible-height">
<div class="user-info">
<div class="container">
<a
v-if="avatarAction === 'zoom'"
class="user-info-avatar -link"
@click="zoomAvatar"
>
<UserAvatar
:better-shadow="betterShadow"
:user="user"
/>
<div class="user-info-avatar -link -overlay">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="search-plus"
/>
</div>
</a>
<UserAvatar
v-else-if="typeof avatarAction === 'function'"
class="user-info-avatar"
:better-shadow="betterShadow"
:user="user"
@click="avatarAction"
/>
<router-link
v-else
:to="userProfileLink(user)"
>
<UserAvatar
:better-shadow="betterShadow"
:user="user"
/>
</router-link>
<div class="user-summary">
<div class="top-line">
<router-link
:to="userProfileLink(user)"
class="user-name"
>
<RichContent
:title="user.name"
:html="user.name"
:emoji="user.emoji"
/>
</router-link>
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
<router-link
v-if="onClose"
:to="userProfileLink(user)"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="expand-alt"
/>
</router-link>
<button
v-if="onClose"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="times"
/>
</button>
</div>
<div class="bottom-line">
<user-link
class="user-screen-name"
:user="user"
/>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
</div>
</div>
<div class="user-meta">
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
>
{{ $t('user_card.follows_you') }}
</div>
<div
v-if="isOtherUser && (loggedIn || !switcher)"
class="highlighter"
>
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColorTx'+user.id"
v-model="userHighlightColor"
class="userHighlightText"
type="text"
>
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColor'+user.id"
v-model="userHighlightColor"
class="userHighlightCl"
type="color"
>
{{ ' ' }}
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
class="userHighlightSel"
>
<option value="disabled">
{{ $t('user_card.highlight.disabled') }}
</option>
<option value="solid">
{{ $t('user_card.highlight.solid') }}
</option>
<option value="striped">
{{ $t('user_card.highlight.striped') }}
</option>
<option value="side">
{{ $t('user_card.highlight.side') }}
</option>
</Select>
</div>
</div>
<div
v-if="loggedIn && isOtherUser"
class="user-interactions"
>
<div class="btn-group">
<FollowButton
:relationship="relationship"
:user="user"
/>
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.subscribing"
class="btn button-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<FAIcon icon="bell" />
</ProgressButton>
<ProgressButton
v-else
class="btn button-default toggled"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<FALayers>
<FAIcon
icon="rss"
transform="left-5 shrink-6 up-3 rotate-20"
flip="horizontal"
/>
<FAIcon
icon="rss"
transform="right-5 shrink-6 up-3 rotate-20"
/>
<FAIcon icon="bell" />
</FALayers>
</ProgressButton>
</template>
</div>
<div>
<button
v-if="relationship.muting"
class="btn button-default btn-block toggled"
:disabled="user.deactivated"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
</button>
<button
v-else
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="muteUser"
>
{{ $t('user_card.mute') }}
</button>
</div>
<div>
<button
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<ModerationTools
v-if="showModerationMenu"
:user="user"
/>
</div>
<div
v-if="!loggedIn && user.is_local"
class="user-interactions"
>
<RemoteFollow :user="user" />
</div>
<UserNote
- v-if="loggedIn && isOtherUser && supportsNote"
+ v-if="loggedIn && isOtherUser && (hasNote || (hasNoteEditor && supportsNote))"
:user="user"
:relationship="relationship"
- :editing="isEditingNote"
+ :editable="hasNoteEditor"
/>
</div>
</div>
<div
v-if="!hideBio"
class="panel-body"
>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
/>
</div>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss" src="./user_card.scss" />
diff --git a/src/components/user_note/user_note.js b/src/components/user_note/user_note.js
index dc5b8e7178..830b2e5948 100644
--- a/src/components/user_note/user_note.js
+++ b/src/components/user_note/user_note.js
@@ -1,44 +1,45 @@
const UserNote = {
props: {
user: Object,
- relationship: Object
+ relationship: Object,
+ editable: Boolean
},
data () {
return {
localNote: '',
editing: false,
frozen: false
}
},
computed: {
shouldShow () {
return this.relationship.note || this.editing
}
},
methods: {
startEditing () {
this.localNote = this.relationship.note
this.editing = true
},
cancelEditing () {
this.editing = false
},
finalizeEditing () {
this.frozen = true
this.$store.dispatch('editUserNote', {
id: this.user.id,
comment: this.localNote
})
.then(() => {
this.frozen = false
this.editing = false
})
.catch(() => {
this.frozen = false
})
}
}
}
export default UserNote
diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue
index 328e2350f5..4286e0170f 100644
--- a/src/components/user_note/user_note.vue
+++ b/src/components/user_note/user_note.vue
@@ -1,89 +1,88 @@
<template>
<div
class="user-note"
>
<div class="heading">
<span>{{ $t('user_card.note') }}</span>
<div class="buttons">
<button
- v-show="!editing"
+ v-show="!editing && editable"
class="button-default btn"
@click="startEditing"
>
{{ $t('user_card.edit_note') }}
</button>
<button
v-show="editing"
class="button-default btn"
:disabled="frozen"
@click="finalizeEditing"
>
{{ $t('user_card.edit_note_apply') }}
</button>
<button
v-show="editing"
class="button-default btn"
:disabled="frozen"
@click="cancelEditing"
>
{{ $t('user_card.edit_note_cancel') }}
</button>
</div>
</div>
<textarea
v-show="editing"
v-model="localNote"
class="note-text"
/>
<span
v-show="!editing"
class="note-text"
:class="{ '-blank': !relationship.note }"
>
{{ relationship.note || $t('user_card.note_blank') }}
</span>
</div>
</template>
<script src="./user_note.js"></script>
<style lang="scss">
@import '../../variables';
.user-note {
display: flex;
flex-direction: column;
.heading {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75em;
.btn {
min-width: 95px;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: right;
.btn {
margin-left: 0.5em;
}
}
}
.note-text {
- line-height: 2;
align-self: stretch;
}
.note-text.-blank {
font-style: italic;
color: var(--faint, $fallback--faint);
}
}
</style>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index d0da2b5b1d..d5e8d23027 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,214 +1,215 @@
<template>
<div>
<div
v-if="user"
class="user-profile panel panel-default"
>
<UserCard
:user-id="userId"
:switcher="true"
:selected="timeline.viewing"
avatar-action="zoom"
rounded="top"
+ :has-note-editor="true"
/>
<div
v-if="user.fields_html && user.fields_html.length > 0"
class="user-profile-fields"
>
<dl
v-for="(field, index) in user.fields_html"
:key="index"
class="user-profile-field"
>
<dt
:title="user.fields_text[index].name"
class="user-profile-field-name"
>
<RichContent
:html="field.name"
:emoji="user.emoji"
/>
</dt>
<dd
:title="user.fields_text[index].value"
class="user-profile-field-value"
>
<RichContent
:html="field.value"
:emoji="user.emoji"
/>
</dd>
</dl>
</div>
<tab-switcher
:active-tab="tab"
:render-only-focused="true"
:on-switch="onTabSwitch"
>
<Timeline
key="statuses"
:label="$t('user_card.statuses')"
:count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
timeline-name="user"
:user-id="userId"
:pinned-status-ids="user.pinnedStatusIds"
:in-profile="true"
:footer-slipgate="footerRef"
/>
<div
v-if="followsTabVisible"
key="followees"
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
<FriendList :user-id="userId">
<template #item="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div>
<div
v-if="followersTabVisible"
key="followers"
:label="$t('user_card.followers')"
:disabled="!user.followers_count"
>
<FollowerList :user-id="userId">
<template #item="{item}">
<FollowCard
:user="item"
:no-follows-you="isUs"
/>
</template>
</FollowerList>
</div>
<Timeline
key="media"
:label="$t('user_card.media')"
:disabled="!media.visibleStatuses.length"
:embedded="true"
:title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
:user-id="userId"
:in-profile="true"
:footer-slipgate="footerRef"
/>
<Timeline
v-if="isUs"
key="favorites"
:label="$t('user_card.favorites')"
:disabled="!favorites.visibleStatuses.length"
:embedded="true"
:title="$t('user_card.favorites')"
timeline-name="favorites"
:timeline="favorites"
:in-profile="true"
:footer-slipgate="footerRef"
/>
</tab-switcher>
<div
:ref="setFooterRef"
class="panel-footer"
/>
</div>
<div
v-else
class="panel user-profile-placeholder"
>
<div class="panel-heading">
<div class="title">
{{ $t('settings.profile_tab') }}
</div>
</div>
<div class="panel-body">
<span v-if="error">{{ error }}</span>
<FAIcon
v-else
spin
icon="circle-notch"
/>
</div>
</div>
</div>
</template>
<script src="./user_profile.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-profile {
flex: 2;
flex-basis: 500px;
// No sticky header on user profile
--currentPanelStack: 1;
.user-profile-fields {
margin: 0 0.5em;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
&.emoji {
width: 18px;
height: 18px;
}
}
.user-profile-field {
display: flex;
margin: 0.25em;
border: 1px solid var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
.user-profile-field-name {
flex: 0 1 30%;
font-weight: 500;
text-align: right;
color: var(--lightText);
min-width: 120px;
border-right: 1px solid var(--border, $fallback--border);
}
.user-profile-field-value {
flex: 1 1 70%;
color: var(--text);
margin: 0 0 0 0.25em;
}
.user-profile-field-name, .user-profile-field-value {
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding: 0.5em 1.5em;
box-sizing: border-box;
}
}
}
.userlist-placeholder {
display: flex;
justify-content: center;
align-items: middle;
padding: 2em;
}
}
.user-profile-placeholder {
.panel-body {
display: flex;
justify-content: center;
align-items: middle;
padding: 7em;
}
}
</style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 30, 2:25 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737701
Default Alt Text
(25 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment