Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sun, Aug 30, 2:25 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737701
Default Alt Text
(25 KB)

Event Timeline