Merge remote-tracking branch 'upstream/develop' into minimal-scopes-mode
* upstream/develop: (173 commits) Fix: Change condition fix typo update store according to retweeted status #433 - update sort by for conversation display replies_count right after reply icon expose replies_count from mastodon api Apparently, MastoAPI gives status in ancestors if you try opening a repeat... make side drawer use gesture service and fix its animations review/remove error hiding errata review #433 - sort conversation for retweets and clean up Revert "Merge branch 'revert-987b5162' into 'develop'" Revert "Merge branch 'mastoapi/friends-tl' into 'develop'" Add await to login action' Remove console log Fix warnings in user profile routing Add tests for gesture service, fix bug with perpendicular directions #255 - clean up autocomplete form #255 - clean up user settings page with self-closing html tags ...
This commit is contained in:
commit
9f4a9bff46
115 changed files with 3612 additions and 1222 deletions
|
@ -160,6 +160,7 @@
|
|||
|
||||
.hider {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -12,7 +12,7 @@ const BasicUserCard = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
UserCardContent,
|
||||
UserCard,
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<div class="user-card">
|
||||
<div class="basic-user-card">
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
||||
</router-link>
|
||||
<div class="user-card-expanded-content" v-if="userExpanded">
|
||||
<user-card-content :user="user" :switcher="false"></user-card-content>
|
||||
<div class="basic-user-card-expanded-content" v-if="userExpanded">
|
||||
<UserCard :user="user" :rounded="true" :bordered="true"/>
|
||||
</div>
|
||||
<div class="user-card-collapsed-content" v-else>
|
||||
<div :title="user.name" class="user-card-user-name">
|
||||
<span v-if="user.name_html" v-html="user.name_html"></span>
|
||||
<span v-else>{{ user.name }}</span>
|
||||
<div class="basic-user-card-collapsed-content" v-else>
|
||||
<div :title="user.name" class="basic-user-card-user-name">
|
||||
<span v-if="user.name_html" class="basic-user-card-user-name-value" v-html="user.name_html"></span>
|
||||
<span v-else class="basic-user-card-user-name-value">{{ user.name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<router-link class="user-card-screen-name" :to="userProfileLink(user)">
|
||||
<router-link class="basic-user-card-screen-name" :to="userProfileLink(user)">
|
||||
@{{user.screen_name}}
|
||||
</router-link>
|
||||
</div>
|
||||
|
@ -26,15 +26,15 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.user-card {
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
margin: 0;
|
||||
padding-top: 0.6em;
|
||||
padding-right: 1em;
|
||||
padding-bottom: 0.6em;
|
||||
padding-left: 1em;
|
||||
border-bottom: 1px solid;
|
||||
margin: 0;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
|
||||
|
@ -52,28 +52,19 @@
|
|||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-value {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&-expanded-content {
|
||||
flex: 1;
|
||||
margin-left: 0.7em;
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
border-style: solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-width: 1px;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,7 @@ const BlockCard = {
|
|||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
return this.user.statusnet_blocking
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Conversation from '../conversation/conversation.vue'
|
||||
import { find } from 'lodash'
|
||||
|
||||
const conversationPage = {
|
||||
components: {
|
||||
|
@ -8,8 +7,8 @@ const conversationPage = {
|
|||
computed: {
|
||||
statusoid () {
|
||||
const id = this.$route.params.id
|
||||
const statuses = this.$store.state.statuses.allStatuses
|
||||
const status = find(statuses, {id})
|
||||
const statuses = this.$store.state.statuses.allStatusesObject
|
||||
const status = statuses[id]
|
||||
|
||||
return status
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<conversation :collapsable="false" :statusoid="statusoid"></conversation>
|
||||
<conversation
|
||||
:collapsable="false"
|
||||
isPage="true"
|
||||
:statusoid="statusoid"
|
||||
></conversation>
|
||||
</template>
|
||||
|
||||
<script src="./conversation-page.js"></script>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { reduce, filter } from 'lodash'
|
||||
import { reduce, filter, findIndex } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import Status from '../status/status.vue'
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const seqA = Number(a.id)
|
||||
const seqB = Number(b.id)
|
||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
|
||||
const seqA = Number(idA)
|
||||
const seqB = Number(idB)
|
||||
const isSeqA = !Number.isNaN(seqA)
|
||||
const isSeqB = !Number.isNaN(seqB)
|
||||
if (isSeqA && isSeqB) {
|
||||
|
@ -13,29 +16,53 @@ const sortById = (a, b) => {
|
|||
} else if (!isSeqA && isSeqB) {
|
||||
return 1
|
||||
} else {
|
||||
return a.id < b.id ? -1 : 1
|
||||
return idA < idB ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
const sortAndFilterConversation = (conversation) => {
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
const sortAndFilterConversation = (conversation, statusoid) => {
|
||||
if (statusoid.type === 'retweet') {
|
||||
conversation = filter(
|
||||
conversation,
|
||||
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
||||
)
|
||||
} else {
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
}
|
||||
return conversation.filter(_ => _).sort(sortById)
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
data () {
|
||||
return {
|
||||
highlight: null
|
||||
highlight: null,
|
||||
expanded: false,
|
||||
converationStatusIds: []
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
'collapsable'
|
||||
'collapsable',
|
||||
'isPage'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
status () {
|
||||
return this.statusoid
|
||||
},
|
||||
idsToShow () {
|
||||
if (this.converationStatusIds.length > 0) {
|
||||
return this.converationStatusIds
|
||||
} else if (this.statusId) {
|
||||
return [this.statusId]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
},
|
||||
statusId () {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return this.statusoid.retweeted_status.id
|
||||
|
@ -48,10 +75,22 @@ const conversation = {
|
|||
return []
|
||||
}
|
||||
|
||||
const conversationId = this.status.statusnet_conversation_id
|
||||
const statuses = this.$store.state.statuses.allStatuses
|
||||
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
|
||||
return sortAndFilterConversation(conversation)
|
||||
if (!this.isExpanded) {
|
||||
return [this.status]
|
||||
}
|
||||
|
||||
const statusesObject = this.$store.state.statuses.allStatusesObject
|
||||
const conversation = this.idsToShow.reduce((acc, id) => {
|
||||
acc.push(statusesObject[id])
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const statusIndex = findIndex(conversation, { id: this.statusId })
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
}
|
||||
|
||||
return sortAndFilterConversation(conversation, this.status)
|
||||
},
|
||||
replies () {
|
||||
let i = 1
|
||||
|
@ -69,23 +108,34 @@ const conversation = {
|
|||
i++
|
||||
return result
|
||||
}, {})
|
||||
},
|
||||
isExpanded () {
|
||||
return this.expanded || this.isPage
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Status
|
||||
},
|
||||
created () {
|
||||
this.fetchConversation()
|
||||
},
|
||||
watch: {
|
||||
'$route': 'fetchConversation'
|
||||
'$route': 'fetchConversation',
|
||||
expanded (value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchConversation () {
|
||||
if (this.status) {
|
||||
const conversationId = this.status.statusnet_conversation_id
|
||||
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
||||
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
||||
this.$store.state.api.backendInteractor.fetchConversation({id: this.status.id})
|
||||
.then(({ancestors, descendants}) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
set(this, 'converationStatusIds', [].concat(
|
||||
ancestors.map(_ => _.id).filter(_ => _ !== this.statusId),
|
||||
this.statusId,
|
||||
descendants.map(_ => _.id).filter(_ => _ !== this.statusId)))
|
||||
})
|
||||
.then(() => this.setHighlight(this.statusId))
|
||||
} else {
|
||||
const id = this.$route.params.id
|
||||
|
@ -98,10 +148,19 @@ const conversation = {
|
|||
return this.replies[id] || []
|
||||
},
|
||||
focused (id) {
|
||||
return id === this.statusId
|
||||
return (this.isExpanded) && id === this.status.id
|
||||
},
|
||||
setHighlight (id) {
|
||||
this.highlight = id
|
||||
},
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
if (!this.expanded) {
|
||||
this.setHighlight(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,42 @@
|
|||
<template>
|
||||
<div class="timeline panel panel-default">
|
||||
<div class="panel-heading conversation-heading">
|
||||
<div class="timeline panel-default" :class="[isExpanded ? 'panel' : 'panel-disabled']">
|
||||
<div v-if="isExpanded" class="panel-heading conversation-heading">
|
||||
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
||||
<span v-if="collapsable">
|
||||
<a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a>
|
||||
<a href="#" @click.prevent="toggleExpanded">{{ $t('timeline.collapse') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="timeline">
|
||||
<status
|
||||
v-for="status in conversation"
|
||||
@goto="setHighlight" :key="status.id"
|
||||
:inlineExpanded="collapsable" :statusoid="status"
|
||||
:expandable='false' :focused="focused(status.id)"
|
||||
:inConversation='true'
|
||||
:highlight="highlight"
|
||||
:replies="getReplies(status.id)"
|
||||
class="status-fadein">
|
||||
</status>
|
||||
</div>
|
||||
</div>
|
||||
<status
|
||||
v-for="status in conversation"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
:key="status.id"
|
||||
:inlineExpanded="collapsable"
|
||||
:statusoid="status"
|
||||
:expandable='!expanded'
|
||||
:focused="focused(status.id)"
|
||||
:inConversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
class="status-fadein panel-body"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./conversation.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.timeline {
|
||||
.panel-disabled {
|
||||
.status-el {
|
||||
border-left: none;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
107
src/components/emoji-input/emoji-input.js
Normal file
107
src/components/emoji-input/emoji-input.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
import Completion from '../../services/completion/completion.js'
|
||||
import { take, filter, map } from 'lodash'
|
||||
|
||||
const EmojiInput = {
|
||||
props: [
|
||||
'value',
|
||||
'placeholder',
|
||||
'type',
|
||||
'classname'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
highlighted: 0,
|
||||
caret: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
suggestions () {
|
||||
const firstchar = this.textAtCaret.charAt(0)
|
||||
if (firstchar === ':') {
|
||||
if (this.textAtCaret === ':') { return }
|
||||
const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
|
||||
if (matchedEmoji.length <= 0) {
|
||||
return false
|
||||
}
|
||||
return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
|
||||
shortcode: `:${shortcode}:`,
|
||||
utf: utf || '',
|
||||
// eslint-disable-next-line camelcase
|
||||
img: utf ? '' : this.$store.state.instance.server + image_url,
|
||||
highlighted: index === this.highlighted
|
||||
}))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
textAtCaret () {
|
||||
return (this.wordAtCaret || {}).word || ''
|
||||
},
|
||||
wordAtCaret () {
|
||||
const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
|
||||
return word
|
||||
},
|
||||
emoji () {
|
||||
return this.$store.state.instance.emoji || []
|
||||
},
|
||||
customEmoji () {
|
||||
return this.$store.state.instance.customEmoji || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
replace (replacement) {
|
||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||
this.$emit('input', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
replaceEmoji (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (this.textAtCaret === ':' || e.ctrlKey) { return }
|
||||
if (len > 0) {
|
||||
e.preventDefault()
|
||||
const emoji = this.suggestions[this.highlighted]
|
||||
const replacement = emoji.utf || (emoji.shortcode + ' ')
|
||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||
this.$emit('input', newValue)
|
||||
this.caret = 0
|
||||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleBackward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 0) {
|
||||
e.preventDefault()
|
||||
this.highlighted -= 1
|
||||
if (this.highlighted < 0) {
|
||||
this.highlighted = this.suggestions.length - 1
|
||||
}
|
||||
} else {
|
||||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleForward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 0) {
|
||||
if (e.shiftKey) { return }
|
||||
e.preventDefault()
|
||||
this.highlighted += 1
|
||||
if (this.highlighted >= len) {
|
||||
this.highlighted = 0
|
||||
}
|
||||
} else {
|
||||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
onKeydown (e) {
|
||||
e.stopPropagation()
|
||||
},
|
||||
onInput (e) {
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
setCaret ({target: {selectionStart}}) {
|
||||
this.caret = selectionStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojiInput
|
64
src/components/emoji-input/emoji-input.vue
Normal file
64
src/components/emoji-input/emoji-input.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="emoji-input">
|
||||
<input
|
||||
v-if="type !== 'textarea'"
|
||||
:class="classname"
|
||||
:type="type"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
@click="setCaret"
|
||||
@keyup="setCaret"
|
||||
@keydown="onKeydown"
|
||||
@keydown.down="cycleForward"
|
||||
@keydown.up="cycleBackward"
|
||||
@keydown.shift.tab="cycleBackward"
|
||||
@keydown.tab="cycleForward"
|
||||
@keydown.enter="replaceEmoji"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
:class="classname"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
@click="setCaret"
|
||||
@keyup="setCaret"
|
||||
@keydown="onKeydown"
|
||||
@keydown.down="cycleForward"
|
||||
@keydown.up="cycleBackward"
|
||||
@keydown.shift.tab="cycleBackward"
|
||||
@keydown.tab="cycleForward"
|
||||
@keydown.enter="replaceEmoji"
|
||||
></textarea>
|
||||
<div class="autocomplete-panel" v-if="suggestions">
|
||||
<div class="autocomplete-panel-body">
|
||||
<div
|
||||
v-for="(emoji, index) in suggestions"
|
||||
:key="index"
|
||||
@click="replace(emoji.utf || (emoji.shortcode + ' '))"
|
||||
class="autocomplete-item"
|
||||
:class="{ highlighted: emoji.highlighted }"
|
||||
>
|
||||
<span v-if="emoji.img">
|
||||
<img :src="emoji.img" />
|
||||
</span>
|
||||
<span v-else>{{emoji.utf}}</span>
|
||||
<span>{{emoji.shortcode}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji-input.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.emoji-input {
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,4 +1,5 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
|
||||
const FollowCard = {
|
||||
|
@ -14,13 +15,17 @@ const FollowCard = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
RemoteFollow
|
||||
},
|
||||
computed: {
|
||||
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
|
||||
following () { return this.updated ? this.updated.following : this.user.following },
|
||||
showFollow () {
|
||||
return !this.following || this.updated && !this.updated.following
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
<div class="follow-card-follow-button" v-if="showFollow && !loggedIn">
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
<button
|
||||
v-if="showFollow"
|
||||
class="btn btn-default"
|
||||
v-if="showFollow && loggedIn"
|
||||
class="btn btn-default follow-card-follow-button"
|
||||
@click="followUser"
|
||||
:disabled="inProgress"
|
||||
:title="requestSent ? $t('user_card.follow_again') : ''"
|
||||
|
@ -21,7 +24,7 @@
|
|||
{{ $t('user_card.follow') }}
|
||||
</template>
|
||||
</button>
|
||||
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="inProgress">
|
||||
<button v-if="following" class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
|
||||
<template v-if="inProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
|
@ -36,15 +39,17 @@
|
|||
<script src="./follow_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.follow-card-content-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.5em;
|
||||
.follow-card {
|
||||
&-content-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
&-follow-button {
|
||||
margin-top: 0.5em;
|
||||
margin-left: auto;
|
||||
width: 10em;
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
align-content: stretch;
|
||||
flex-grow: 1;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
.attachments, .attachment {
|
||||
margin: 0 0.5em 0 0;
|
||||
|
|
|
@ -31,6 +31,9 @@ const ImageCropper = {
|
|||
saveButtonLabel: {
|
||||
type: String
|
||||
},
|
||||
saveWithoutCroppingButtonlabel: {
|
||||
type: String
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
type: String
|
||||
}
|
||||
|
@ -48,6 +51,9 @@ const ImageCropper = {
|
|||
saveText () {
|
||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText () {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
},
|
||||
cancelText () {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
},
|
||||
|
@ -67,7 +73,19 @@ const ImageCropper = {
|
|||
submit () {
|
||||
this.submitting = true
|
||||
this.avatarUploadError = null
|
||||
this.submitHandler(this.cropper, this.filename)
|
||||
this.submitHandler(this.cropper, this.file)
|
||||
.then(() => this.destroy())
|
||||
.catch((err) => {
|
||||
this.submitError = err
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
submitWithoutCropping () {
|
||||
this.submitting = true
|
||||
this.avatarUploadError = null
|
||||
this.submitHandler(false, this.dataUrl)
|
||||
.then(() => this.destroy())
|
||||
.catch((err) => {
|
||||
this.submitError = err
|
||||
|
@ -88,14 +106,14 @@ const ImageCropper = {
|
|||
readFile () {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
this.file = fileInput.files[0]
|
||||
let reader = new window.FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.dataUrl = e.target.result
|
||||
this.$emit('open')
|
||||
}
|
||||
reader.readAsDataURL(fileInput.files[0])
|
||||
this.filename = fileInput.files[0].name || 'unknown'
|
||||
this.$emit('changed', fileInput.files[0], reader)
|
||||
reader.readAsDataURL(this.file)
|
||||
this.$emit('changed', this.file, reader)
|
||||
}
|
||||
},
|
||||
clearError () {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<div class="image-cropper-buttons-wrapper">
|
||||
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
|
||||
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
||||
<button class="btn" type="button" :disabled="submitting" @click="submitWithoutCropping" v-text="saveWithoutCroppingText"></button>
|
||||
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
||||
</div>
|
||||
<div class="alert error" v-if="submitError">
|
||||
|
@ -36,7 +37,11 @@
|
|||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
margin-top: 15px;
|
||||
margin-top: 10px;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.card-image {
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="modal-view" v-if="showing" @click.prevent="hide">
|
||||
<div class="modal-view media-modal-view" v-if="showing" @click.prevent="hide">
|
||||
<img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
|
||||
<VideoAttachment
|
||||
class="modal-image"
|
||||
|
@ -32,18 +32,7 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.modal-view {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.media-modal-view {
|
||||
&:hover {
|
||||
.modal-view-button-arrow {
|
||||
opacity: 0.75;
|
||||
|
|
|
@ -20,7 +20,7 @@ const mediaUpload = {
|
|||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('media', file)
|
||||
formData.append('file', file)
|
||||
|
||||
self.$emit('uploading')
|
||||
self.uploading = true
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const MobilePostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
postFormOpen: false,
|
||||
scrollingDown: false,
|
||||
inputActive: false,
|
||||
oldScrollPos: 0,
|
||||
amountScrolled: 0
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
window.addEventListener('resize', this.handleOSK)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
isHidden () {
|
||||
return this.hidden || this.inputActive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPostForm () {
|
||||
this.postFormOpen = true
|
||||
this.hidden = true
|
||||
|
||||
const el = this.$el.querySelector('textarea')
|
||||
this.$nextTick(function () {
|
||||
el.focus()
|
||||
})
|
||||
},
|
||||
closePostForm () {
|
||||
this.postFormOpen = false
|
||||
this.hidden = false
|
||||
},
|
||||
handleOSK () {
|
||||
// This is a big hack: we're guessing from changed window sizes if the
|
||||
// on-screen keyboard is active or not. This is only really important
|
||||
// for phones in portrait mode and it's more important to show the button
|
||||
// in normal scenarios on all phones, than it is to hide it when the
|
||||
// keyboard is active.
|
||||
// Guesswork based on https://www.mydevice.io/#compare-devices
|
||||
|
||||
// for example, iphone 4 and android phones from the same time period
|
||||
const smallPhone = window.innerWidth < 350
|
||||
const smallPhoneKbOpen = smallPhone && window.innerHeight < 345
|
||||
|
||||
const biggerPhone = !smallPhone && window.innerWidth < 450
|
||||
const biggerPhoneKbOpen = biggerPhone && window.innerHeight < 560
|
||||
if (smallPhoneKbOpen || biggerPhoneKbOpen) {
|
||||
this.inputActive = true
|
||||
} else {
|
||||
this.inputActive = false
|
||||
}
|
||||
},
|
||||
handleScroll: throttle(function () {
|
||||
const scrollAmount = window.scrollY - this.oldScrollPos
|
||||
const scrollingDown = scrollAmount > 0
|
||||
|
||||
if (scrollingDown !== this.scrollingDown) {
|
||||
this.amountScrolled = 0
|
||||
this.scrollingDown = scrollingDown
|
||||
if (!scrollingDown) {
|
||||
this.hidden = false
|
||||
}
|
||||
} else if (scrollingDown) {
|
||||
this.amountScrolled += scrollAmount
|
||||
if (this.amountScrolled > 100 && !this.hidden) {
|
||||
this.hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
this.oldScrollPos = window.scrollY
|
||||
this.scrollingDown = scrollingDown
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
export default MobilePostStatusModal
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div v-if="currentUser">
|
||||
<div
|
||||
class="post-form-modal-view modal-view"
|
||||
v-show="postFormOpen"
|
||||
@click="closePostForm"
|
||||
>
|
||||
<div class="post-form-modal-panel panel" @click.stop="">
|
||||
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
|
||||
<PostStatusForm class="panel-body" @posted="closePostForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="new-status-button"
|
||||
:class="{ 'hidden': isHidden }"
|
||||
@click="openPostForm"
|
||||
>
|
||||
<i class="icon-edit" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mobile_post_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin: 25% 0 4em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
border-radius: 100%;
|
||||
position: fixed;
|
||||
bottom: 1.5em;
|
||||
right: 1.5em;
|
||||
// TODO: this needs its own color, it has to stand out enough and link color
|
||||
// is not very optimal for this particular use.
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
|
||||
transition: 0.35s transform;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
|
||||
&.hidden {
|
||||
transform: translateY(150%);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.5em;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
.new-status-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -9,7 +9,7 @@ const MuteCard = {
|
|||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
muted () {
|
||||
return this.user.muted
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<template slot="secondary-area">
|
||||
<div class="mute-card-content-container">
|
||||
<button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
|
@ -17,8 +17,18 @@
|
|||
{{ $t('user_card.mute') }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
<script src="./mute_card.js"></script>
|
||||
<script src="./mute_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.mute-card-content-container {
|
||||
margin-top: 0.5em;
|
||||
text-align: right;
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Status from '../status/status.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -13,7 +13,7 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status, UserAvatar, UserCardContent
|
||||
Status, UserAvatar, UserCard
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
|
||||
</a>
|
||||
<div class='notification-right'>
|
||||
<div class="usercard notification-usercard" v-if="userExpanded">
|
||||
<user-card-content :user="notification.action.user" :switcher="false"></user-card-content>
|
||||
</div>
|
||||
<UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
|
||||
|
@ -25,7 +23,11 @@
|
|||
<small>{{$t('notifications.followed_you')}}</small>
|
||||
</span>
|
||||
</div>
|
||||
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||
<div class="timeago">
|
||||
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
|
||||
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||
<router-link :to="userProfileLink(notification.action.user)">
|
||||
|
|
|
@ -11,7 +11,8 @@ const Notifications = {
|
|||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
|
||||
notificationsFetcher.startFetching({ store, credentials })
|
||||
const fetcherId = notificationsFetcher.startFetching({ store, credentials })
|
||||
this.$store.commit('setNotificationFetcher', { fetcherId })
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -45,10 +45,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.notification-usercard {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -126,7 +122,7 @@
|
|||
}
|
||||
|
||||
.timeago {
|
||||
font-size: 12px;
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.icon-retweet.lit {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||
import MediaUpload from '../media_upload/media_upload.vue'
|
||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import Completion from '../../services/completion/completion.js'
|
||||
import { take, filter, reject, map, uniqBy } from 'lodash'
|
||||
|
@ -30,7 +31,8 @@ const PostStatusForm = {
|
|||
],
|
||||
components: {
|
||||
MediaUpload,
|
||||
ScopeSelector
|
||||
ScopeSelector,
|
||||
EmojiInput
|
||||
},
|
||||
mounted () {
|
||||
this.resize(this.$refs.textarea)
|
||||
|
@ -174,6 +176,9 @@ const PostStatusForm = {
|
|||
},
|
||||
formattingOptionsEnabled () {
|
||||
return this.$store.state.instance.formattingOptionsEnabled
|
||||
},
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -222,6 +227,9 @@ const PostStatusForm = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
onKeydown (e) {
|
||||
e.stopPropagation()
|
||||
},
|
||||
setCaret ({target: {selectionStart}}) {
|
||||
this.caret = selectionStart
|
||||
},
|
||||
|
@ -293,6 +301,8 @@ const PostStatusForm = {
|
|||
},
|
||||
paste (e) {
|
||||
if (e.clipboardData.files.length > 0) {
|
||||
// prevent pasting of file as text
|
||||
e.preventDefault()
|
||||
// Strangely, files property gets emptied after event propagation
|
||||
// Trying to wrap it in array doesn't work. Plus I doubt it's possible
|
||||
// to hold more than one file in clipboard.
|
||||
|
|
|
@ -10,16 +10,18 @@
|
|||
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
||||
</i18n>
|
||||
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
|
||||
<input
|
||||
<EmojiInput
|
||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
v-model="newStatus.spoilerText"
|
||||
class="form-cw">
|
||||
classname="form-control"
|
||||
/>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
@click="setCaret"
|
||||
@keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
|
||||
@keydown="onKeydown"
|
||||
@keydown.down="cycleForward"
|
||||
@keydown.up="cycleBackward"
|
||||
@keydown.shift.tab="cycleBackward"
|
||||
|
@ -30,15 +32,17 @@
|
|||
@drop="fileDrop"
|
||||
@dragover.prevent="fileDrag"
|
||||
@input="resize"
|
||||
@paste="paste">
|
||||
@paste="paste"
|
||||
:disabled="posting"
|
||||
>
|
||||
</textarea>
|
||||
<div class="visibility-tray">
|
||||
<span class="text-format" v-if="formattingOptionsEnabled">
|
||||
<label for="post-content-type" class="select">
|
||||
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
||||
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
|
||||
<option value="text/html">HTML</option>
|
||||
<option value="text/markdown">Markdown</option>
|
||||
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
||||
{{$t(`post_status.content_type["${postFormat}"]`)}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"></i>
|
||||
</label>
|
||||
|
@ -52,14 +56,18 @@
|
|||
:onScopeChange="changeVis"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative;" v-if="candidates">
|
||||
<div class="autocomplete-panel">
|
||||
<div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
|
||||
<div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
|
||||
<span v-if="candidate.img"><img :src="candidate.img"></img></span>
|
||||
<span v-else>{{candidate.utf}}</span>
|
||||
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
||||
</div>
|
||||
<div class="autocomplete-panel" v-if="candidates">
|
||||
<div class="autocomplete-panel-body">
|
||||
<div
|
||||
v-for="(candidate, index) in candidates"
|
||||
:key="index"
|
||||
@click="replace(candidate.utf || (candidate.screen_name + ' '))"
|
||||
class="autocomplete-item"
|
||||
:class="{ highlighted: candidate.highlighted }"
|
||||
>
|
||||
<span v-if="candidate.img"><img :src="candidate.img" /></span>
|
||||
<span v-else>{{candidate.utf}}</span>
|
||||
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -81,10 +89,10 @@
|
|||
<div class="media-upload-wrapper" v-for="file in newStatus.files">
|
||||
<i class="fa button-icon icon-cancel" @click="removeMediaFile(file)"></i>
|
||||
<div class="media-upload-container attachment">
|
||||
<img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
|
||||
<video v-if="type(file) === 'video'" :src="file.image" controls></video>
|
||||
<audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
|
||||
<a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
|
||||
<img class="thumbnail media-upload" :src="file.url" v-if="type(file) === 'image'"></img>
|
||||
<video v-if="type(file) === 'video'" :src="file.url" controls></video>
|
||||
<audio v-if="type(file) === 'audio'" :src="file.url" controls></audio>
|
||||
<a v-if="type(file) === 'unknown'" :href="file.url">{{file.url}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -258,52 +266,5 @@
|
|||
cursor: pointer;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.autocomplete-panel {
|
||||
margin: 0 0.5em 0 0.5em;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
// this doesn't match original but i don't care, making it uniform.
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background: $fallback--bg;
|
||||
background: var(--bg, $fallback--bg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
cursor: pointer;
|
||||
padding: 0.2em 0.4em 0.2em 0.4em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 24px;
|
||||
margin: 0 0.1em 0 0.2em;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-left: .5em;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--lightBg, $fallback--fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -35,6 +35,9 @@ const registration = {
|
|||
},
|
||||
computed: {
|
||||
token () { return this.$route.params.token },
|
||||
bioPlaceholder () {
|
||||
return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n')
|
||||
},
|
||||
...mapState({
|
||||
registrationOpen: (state) => state.instance.registrationOpen,
|
||||
signedIn: (state) => !!state.users.currentUser,
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<div class='form-group'>
|
||||
<label class='form--label' for='bio'>{{$t('registration.bio')}} ({{$t('general.optional')}})</label>
|
||||
<textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="$t('registration.bio_placeholder')"></textarea>
|
||||
<textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="bioPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
|
||||
|
|
10
src/components/remote_follow/remote_follow.js
Normal file
10
src/components/remote_follow/remote_follow.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
props: [ 'user' ],
|
||||
computed: {
|
||||
subscribeUrl () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const serverUrl = new URL(this.user.statusnet_profile_url)
|
||||
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
|
||||
}
|
||||
}
|
||||
}
|
24
src/components/remote_follow/remote_follow.vue
Normal file
24
src/components/remote_follow/remote_follow.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="remote-follow">
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./remote_follow.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.remote-follow {
|
||||
max-width: 220px;
|
||||
|
||||
.remote-button {
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,13 @@
|
|||
/* eslint-env browser */
|
||||
import { filter, trim } from 'lodash'
|
||||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import { filter, trim } from 'lodash'
|
||||
import { extractCommit } from '../../services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
||||
|
||||
const settings = {
|
||||
data () {
|
||||
|
@ -42,6 +47,11 @@ const settings = {
|
|||
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
|
||||
hoverPreviewLocal: user.hoverPreview,
|
||||
|
||||
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
|
||||
? instance.hideMutedPosts
|
||||
: user.hideMutedPosts,
|
||||
hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts),
|
||||
|
||||
collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined'
|
||||
? instance.collapseMessageWithSubject
|
||||
: user.collapseMessageWithSubject,
|
||||
|
@ -83,7 +93,10 @@ const settings = {
|
|||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
||||
playVideosInModal: user.playVideosInModal,
|
||||
useContainFit: user.useContainFit
|
||||
useContainFit: user.useContainFit,
|
||||
|
||||
backendVersion: instance.backendVersion,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -98,7 +111,16 @@ const settings = {
|
|||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hideAttachmentsLocal (value) {
|
||||
|
@ -165,6 +187,9 @@ const settings = {
|
|||
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
||||
},
|
||||
hideMutedPostsLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
|
||||
},
|
||||
collapseMessageWithSubjectLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
|
||||
},
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
<div class="setting-item">
|
||||
<h2>{{$t('nav.timeline')}}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<input type="checkbox" id="hideMutedPosts" v-model="hideMutedPostsLocal">
|
||||
<label for="hideMutedPosts">{{$t('settings.hide_muted_posts')}} {{$t('settings.instance_default', { value: hideMutedPostsDefault })}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
|
||||
<label for="collapseMessageWithSubject">
|
||||
|
@ -105,17 +109,9 @@
|
|||
{{$t('settings.post_status_content_type')}}
|
||||
<label for="postContentType" class="select">
|
||||
<select id="postContentType" v-model="postContentTypeLocal">
|
||||
<option value="text/plain">
|
||||
{{$t('settings.status_content_type_plain')}}
|
||||
{{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
<option value="text/html">
|
||||
HTML
|
||||
{{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
<option value="text/markdown">
|
||||
Markdown
|
||||
{{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}}
|
||||
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
||||
{{$t(`post_status.content_type["${postFormat}"]`)}}
|
||||
{{postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
|
@ -275,6 +271,28 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.version.title')" >
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{$t('settings.version.backend_version')}}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a :href="backendVersionLink" target="_blank">{{backendVersion}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{$t('settings.version.frontend_version')}}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a :href="frontendVersionLink" target="_blank">{{frontendVersion}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
|
||||
// TODO: separate touch gesture stuff into their own utils if more components want them
|
||||
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
||||
|
||||
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
|
||||
const SideDrawer = {
|
||||
props: [ 'logout' ],
|
||||
data: () => ({
|
||||
closed: true,
|
||||
touchCoord: [0, 0]
|
||||
closeGesture: undefined
|
||||
}),
|
||||
components: { UserCardContent },
|
||||
created () {
|
||||
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
||||
},
|
||||
components: { UserCard },
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
|
@ -46,13 +45,10 @@ const SideDrawer = {
|
|||
this.toggleDrawer()
|
||||
},
|
||||
touchStart (e) {
|
||||
this.touchCoord = touchEventCoord(e)
|
||||
GestureService.beginSwipe(e, this.closeGesture)
|
||||
},
|
||||
touchMove (e) {
|
||||
const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
|
||||
if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
|
||||
this.toggleDrawer()
|
||||
}
|
||||
GestureService.updateSwipe(e, this.closeGesture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,21 @@
|
|||
<div class="side-drawer-container"
|
||||
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
|
||||
>
|
||||
<div class="side-drawer-darken" :class="{ 'side-drawer-darken-closed': closed}" />
|
||||
<div class="side-drawer"
|
||||
:class="{'side-drawer-closed': closed}"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
>
|
||||
<div class="side-drawer-heading" @click="toggleDrawer">
|
||||
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/>
|
||||
<UserCard :user="currentUser" :hideBio="true" v-if="currentUser"/>
|
||||
<div class="side-drawer-logo-wrapper" v-else>
|
||||
<img :src="logo"/>
|
||||
<span>{{sitename}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("post_status.new_status") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-else @click="toggleDrawer">
|
||||
<li v-if="!currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'login' }">
|
||||
{{ $t("login.login") }}
|
||||
</router-link>
|
||||
|
@ -116,17 +112,33 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
transition-duration: 0s;
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.side-drawer-container-open {
|
||||
transition-delay: 0.0s;
|
||||
transition-property: left;
|
||||
transform: translate(0%);
|
||||
}
|
||||
|
||||
.side-drawer-container-closed {
|
||||
left: -100%;
|
||||
transition-delay: 0.5s;
|
||||
transition-property: left;
|
||||
transition-delay: 0.35s;
|
||||
transform: translate(-100%);
|
||||
}
|
||||
|
||||
.side-drawer-darken {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
transition: 0.35s;
|
||||
transition-property: background-color;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.side-drawer-darken-closed {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.side-drawer-click-outside {
|
||||
|
@ -135,8 +147,9 @@
|
|||
|
||||
.side-drawer {
|
||||
overflow-x: hidden;
|
||||
transition: 0.35s;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
transition: 0.35s;
|
||||
transition-property: transform;
|
||||
margin: 0 0 0 -100px;
|
||||
padding: 0 0 1em 100px;
|
||||
width: 80%;
|
||||
|
@ -181,15 +194,6 @@
|
|||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.profile-panel-background {
|
||||
border-radius: 0;
|
||||
.panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-drawer ul {
|
||||
|
|
|
@ -3,7 +3,7 @@ import FavoriteButton from '../favorite_button/favorite_button.vue'
|
|||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import DeleteButton from '../delete_button/delete_button.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
|
@ -145,11 +145,11 @@ const Status = {
|
|||
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
||||
},
|
||||
replyToName () {
|
||||
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
|
||||
if (user) {
|
||||
return user.screen_name
|
||||
} else {
|
||||
if (this.status.in_reply_to_screen_name) {
|
||||
return this.status.in_reply_to_screen_name
|
||||
} else {
|
||||
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
|
||||
return user && user.screen_name
|
||||
}
|
||||
},
|
||||
hideReply () {
|
||||
|
@ -259,7 +259,7 @@ const Status = {
|
|||
RetweetButton,
|
||||
DeleteButton,
|
||||
PostStatusForm,
|
||||
UserCardContent,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
|
@ -310,7 +310,6 @@ const Status = {
|
|||
this.replying = !this.replying
|
||||
},
|
||||
gotoOriginal (id) {
|
||||
// only handled by conversation, not status_or_conversation
|
||||
if (this.inConversation) {
|
||||
this.$emit('goto', id)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
||||
<div class="media-body faint">
|
||||
<span class="user-name">
|
||||
|
@ -24,16 +24,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
|
||||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status">
|
||||
<div v-if="!noHeading" class="media-left">
|
||||
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
||||
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
<div class="usercard" v-if="userExpanded">
|
||||
<user-card-content :user="status.user" :switcher="false"></user-card-content>
|
||||
</div>
|
||||
<UserCard :user="status.user" :rounded="true" :bordered="true" class="status-usercard" v-if="userExpanded"/>
|
||||
<div v-if="!noHeading" class="media-heading">
|
||||
<div class="heading-name-row">
|
||||
<div class="name-and-account-name">
|
||||
|
@ -77,13 +75,13 @@
|
|||
<router-link :to="replyProfileLink">
|
||||
{{replyToName}}
|
||||
</router-link>
|
||||
<span class="faint replies-separator" v-if="replies.length">
|
||||
<span class="faint replies-separator" v-if="replies && replies.length">
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
<div class="replies" v-if="inConversation && !isPreview">
|
||||
<span class="faint" v-if="replies.length">{{$t('status.replies_list')}}</span>
|
||||
<span class="reply-link faint" v-for="reply in replies">
|
||||
<span class="faint" v-if="replies && replies.length">{{$t('status.replies_list')}}</span>
|
||||
<span class="reply-link faint" v-if="replies" v-for="reply in replies">
|
||||
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -137,9 +135,8 @@
|
|||
|
||||
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
|
||||
<div v-if="loggedIn">
|
||||
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
|
||||
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
|
||||
</a>
|
||||
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
|
||||
<span v-if="status.replies_count > 0">{{status.replies_count}}</span>
|
||||
</div>
|
||||
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
|
||||
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
|
||||
|
@ -248,8 +245,7 @@ $status-margin: 0.75em;
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.usercard {
|
||||
margin: 0;
|
||||
.status-usercard {
|
||||
margin-bottom: $status-margin;
|
||||
}
|
||||
|
||||
|
@ -422,6 +418,11 @@ $status-margin: 0.75em;
|
|||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
|
@ -549,6 +550,7 @@ $status-margin: 0.75em;
|
|||
.icon-reply:hover {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-reply.icon-reply-active {
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import Status from '../status/status.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
|
||||
const statusOrConversation = {
|
||||
props: ['statusoid'],
|
||||
data () {
|
||||
return {
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
Conversation
|
||||
},
|
||||
methods: {
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default statusOrConversation
|
|
@ -1,14 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<conversation v-if="expanded" @toggleExpanded="toggleExpanded" :collapsable="true" :statusoid="statusoid"></conversation>
|
||||
<status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :inConversation="false" :focused="false" :statusoid="statusoid"></status>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./status_or_conversation.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.spacer {
|
||||
height: 1em
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
import Status from '../status/status.vue'
|
||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const Timeline = {
|
||||
|
@ -43,7 +43,7 @@ const Timeline = {
|
|||
},
|
||||
components: {
|
||||
Status,
|
||||
StatusOrConversation
|
||||
Conversation
|
||||
},
|
||||
created () {
|
||||
const store = this.$store
|
||||
|
@ -132,7 +132,9 @@ const Timeline = {
|
|||
}
|
||||
if (count > 0) {
|
||||
// only 'stream' them when you're scrolled to the top
|
||||
if (window.pageYOffset < 15 &&
|
||||
const doc = document.documentElement
|
||||
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
|
||||
if (top < 15 &&
|
||||
!this.paused &&
|
||||
!(this.unfocused && this.$store.state.config.pauseOnUnfocused)
|
||||
) {
|
||||
|
|
|
@ -16,7 +16,13 @@
|
|||
</div>
|
||||
<div :class="classes.body">
|
||||
<div class="timeline">
|
||||
<status-or-conversation v-for="status in timeline.visibleStatuses" :key="status.id" v-bind:statusoid="status" class="status-fadein"></status-or-conversation>
|
||||
<conversation
|
||||
v-for="status in timeline.visibleStatuses"
|
||||
class="status-fadein"
|
||||
:key="status.id"
|
||||
:statusoid="status"
|
||||
:collapsable="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="classes.footer">
|
||||
|
|
|
@ -23,6 +23,11 @@ const UserAvatar = {
|
|||
imageLoadError () {
|
||||
this.showPlaceholder = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src () {
|
||||
this.showPlaceholder = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
export default {
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ],
|
||||
data () {
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
|
@ -15,8 +16,18 @@ export default {
|
|||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
headingStyle () {
|
||||
classes () {
|
||||
return [{
|
||||
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
||||
'user-card-rounded': this.rounded === true, // set border-radius for all sides
|
||||
'user-card-bordered': this.bordered === true // set border for all sides
|
||||
}]
|
||||
},
|
||||
style () {
|
||||
const color = this.$store.state.config.customTheme.colors
|
||||
? this.$store.state.config.customTheme.colors.bg // v2
|
||||
: this.$store.state.config.colors.bg // v1
|
||||
|
@ -89,36 +100,37 @@ export default {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
RemoteFollow
|
||||
},
|
||||
methods: {
|
||||
followUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
requestFollow(this.user, this.$store).then(({sent}) => {
|
||||
requestFollow(this.user, store).then(({sent}) => {
|
||||
this.followRequestInProgress = false
|
||||
this.followRequestSent = sent
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
requestUnfollow(this.user, this.$store).then(() => {
|
||||
requestUnfollow(this.user, store).then(() => {
|
||||
this.followRequestInProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
|
||||
})
|
||||
},
|
||||
blockUser () {
|
||||
const store = this.$store
|
||||
store.state.api.backendInteractor.blockUser(this.user.id)
|
||||
.then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
},
|
||||
unblockUser () {
|
||||
const store = this.$store
|
||||
store.state.api.backendInteractor.unblockUser(this.user.id)
|
||||
.then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
toggleMute () {
|
||||
const store = this.$store
|
||||
store.commit('setMuted', {user: this.user, muted: !this.user.muted})
|
||||
store.state.api.backendInteractor.setUserMute(this.user)
|
||||
muteUser () {
|
||||
this.$store.dispatch('muteUser', this.user.id)
|
||||
},
|
||||
unmuteUser () {
|
||||
this.$store.dispatch('unmuteUser', this.user.id)
|
||||
},
|
||||
setProfileView (v) {
|
||||
if (this.switcher) {
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div id="heading" class="profile-panel-background" :style="headingStyle">
|
||||
<div class="panel-heading text-center">
|
||||
<div class="user-card" :class="classes" :style="style">
|
||||
<div class="panel-heading">
|
||||
<div class='user-info'>
|
||||
<div class='container'>
|
||||
<router-link :to="userProfileLink(user)">
|
||||
|
@ -11,7 +11,7 @@
|
|||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
<i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
|
@ -74,24 +74,18 @@
|
|||
</div>
|
||||
<div class='mute' v-if='isOtherUser && loggedIn'>
|
||||
<span v-if='user.muted'>
|
||||
<button @click="toggleMute" class="pressed">
|
||||
<button @click="unmuteUser" class="pressed">
|
||||
{{ $t('user_card.muted') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if='!user.muted'>
|
||||
<button @click="toggleMute">
|
||||
<button @click="muteUser">
|
||||
{{ $t('user_card.mute') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remote-follow" v-if='!loggedIn && user.is_local'>
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if='!loggedIn && user.is_local'>
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
<div class='block' v-if='isOtherUser && loggedIn'>
|
||||
<span v-if='user.statusnet_blocking'>
|
||||
|
@ -108,7 +102,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body profile-panel-body" v-if="!hideBio">
|
||||
<div class="panel-body" v-if="!hideBio">
|
||||
<div v-if="!hideUserStatsLocal && switcher" class="user-counts">
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('statuses')">
|
||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||
|
@ -123,40 +117,75 @@
|
|||
<span>{{user.followers_count}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
|
||||
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
|
||||
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="user-card-bio" v-html="user.description_html"></p>
|
||||
<p v-else-if="!hideBio" class="user-card-bio">{{ user.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_card_content.js"></script>
|
||||
<script src="./user_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.profile-panel-background {
|
||||
.user-card {
|
||||
background-size: cover;
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
overflow: hidden;
|
||||
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.panel-heading {
|
||||
padding: .5em 0;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-panel-body {
|
||||
word-wrap: break-word;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
|
||||
.panel-body {
|
||||
word-wrap: break-word;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
|
||||
}
|
||||
|
||||
.profile-bio {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-bio {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
|
||||
&-rounded-t {
|
||||
border-top-left-radius: $fallback--panelRadius;
|
||||
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
border-top-right-radius: $fallback--panelRadius;
|
||||
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
}
|
||||
|
||||
&-rounded {
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
}
|
||||
|
||||
&-bordered {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,11 +369,6 @@
|
|||
min-height: 28px;
|
||||
}
|
||||
|
||||
.remote-follow {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.follow {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
|
@ -393,25 +417,4 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.usercard {
|
||||
width: fill-available;
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
border-style: solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-width: 1px;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
import LoginForm from '../login_form/login_form.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
|
||||
const UserPanel = {
|
||||
computed: {
|
||||
|
@ -9,7 +9,7 @@ const UserPanel = {
|
|||
components: {
|
||||
LoginForm,
|
||||
PostStatusForm,
|
||||
UserCardContent
|
||||
UserCard
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="user-panel">
|
||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
||||
<user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
|
||||
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
||||
<div class="panel-footer">
|
||||
<post-status-form v-if='user'></post-status-form>
|
||||
</div>
|
||||
|
@ -11,13 +11,3 @@
|
|||
</template>
|
||||
|
||||
<script src="./user_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-panel {
|
||||
.profile-panel-background .panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { compose } from 'vue-compose'
|
||||
import get from 'lodash/get'
|
||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
|
@ -9,7 +9,7 @@ import withList from '../../hocs/with_list/with_list'
|
|||
const FollowerList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
|
||||
select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
|
@ -20,7 +20,7 @@ const FollowerList = compose(
|
|||
const FriendList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
|
@ -31,28 +31,16 @@ const FriendList = compose(
|
|||
const UserProfile = {
|
||||
data () {
|
||||
return {
|
||||
error: false
|
||||
error: false,
|
||||
fetchedUserId: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
this.startFetchFavorites()
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
this.error = this.$t('user_profile.profile_does_not_exist')
|
||||
} else if (errorMessage) {
|
||||
this.error = errorMessage
|
||||
} else {
|
||||
this.error = this.$t('user_profile.profile_loading_error')
|
||||
}
|
||||
})
|
||||
this.fetchUserId()
|
||||
.then(() => this.startUp())
|
||||
} else {
|
||||
this.startUp()
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
|
@ -69,7 +57,7 @@ const UserProfile = {
|
|||
return this.$store.state.statuses.timelines.media
|
||||
},
|
||||
userId () {
|
||||
return this.$route.params.id || this.user.id
|
||||
return this.$route.params.id || this.user.id || this.fetchedUserId
|
||||
},
|
||||
userName () {
|
||||
return this.$route.params.name || this.user.screen_name
|
||||
|
@ -79,10 +67,9 @@ const UserProfile = {
|
|||
this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
userInStore () {
|
||||
if (this.isExternal) {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
}
|
||||
return this.$store.getters.userByName(this.userName)
|
||||
const routeParams = this.$route.params
|
||||
// This needs fetchedUserId so that computed will be refreshed when user is fetched
|
||||
return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
|
||||
},
|
||||
user () {
|
||||
if (this.timeline.statuses[0]) {
|
||||
|
@ -93,9 +80,6 @@ const UserProfile = {
|
|||
}
|
||||
return {}
|
||||
},
|
||||
fetchBy () {
|
||||
return this.isExternal ? this.userId : this.userName
|
||||
},
|
||||
isExternal () {
|
||||
return this.$route.name === 'external-user-profile'
|
||||
},
|
||||
|
@ -109,14 +93,38 @@ const UserProfile = {
|
|||
methods: {
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId })
|
||||
}
|
||||
},
|
||||
fetchUserId () {
|
||||
let fetchPromise
|
||||
if (this.userId && !this.$route.params.name) {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
|
||||
} else {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
|
||||
.then(({ id }) => {
|
||||
this.fetchedUserId = id
|
||||
})
|
||||
}
|
||||
return fetchPromise
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
this.error = this.$t('user_profile.profile_does_not_exist')
|
||||
} else if (errorMessage) {
|
||||
this.error = errorMessage
|
||||
} else {
|
||||
this.error = this.$t('user_profile.profile_loading_error')
|
||||
}
|
||||
})
|
||||
.then(() => this.startUp())
|
||||
},
|
||||
startUp () {
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
|
||||
this.startFetchFavorites()
|
||||
if (this.userId) {
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId })
|
||||
this.startFetchFavorites()
|
||||
}
|
||||
},
|
||||
cleanUp () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
|
@ -128,23 +136,26 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
userName () {
|
||||
if (this.isExternal) {
|
||||
return
|
||||
// userId can be undefined if we don't know it yet
|
||||
userId (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
}
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
},
|
||||
userId () {
|
||||
if (!this.isExternal) {
|
||||
return
|
||||
userName () {
|
||||
if (this.$route.params.name) {
|
||||
this.fetchUserId()
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
}
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
},
|
||||
$route () {
|
||||
this.$refs.tabSwitcher.activateTab(0)()
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCardContent,
|
||||
UserCard,
|
||||
Timeline,
|
||||
FollowerList,
|
||||
FriendList
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="user.id" class="user-profile panel panel-default">
|
||||
<user-card-content
|
||||
:user="user"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
/>
|
||||
<tab-switcher :renderOnlyFocused="true">
|
||||
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
|
||||
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
|
||||
<Timeline
|
||||
:label="$t('user_card.statuses')"
|
||||
:disabled="!user.statuses_count"
|
||||
|
@ -15,7 +11,7 @@
|
|||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
:user-id="fetchBy"
|
||||
:user-id="userId"
|
||||
/>
|
||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||
<FriendList :userId="userId" />
|
||||
|
@ -29,7 +25,7 @@
|
|||
:embedded="true" :title="$t('user_card.media')"
|
||||
timeline-name="media"
|
||||
:timeline="media"
|
||||
:user-id="fetchBy"
|
||||
:user-id="userId"
|
||||
/>
|
||||
<Timeline
|
||||
v-if="isUs"
|
||||
|
@ -64,11 +60,6 @@
|
|||
flex: 2;
|
||||
flex-basis: 500px;
|
||||
|
||||
.profile-panel-background .panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.userlist-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -8,6 +8,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
|
|||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
import BlockCard from '../block_card/block_card.vue'
|
||||
import MuteCard from '../mute_card/mute_card.vue'
|
||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import withList from '../../hocs/with_list/with_list'
|
||||
|
||||
|
@ -71,7 +72,8 @@ const UserSettings = {
|
|||
TabSwitcher,
|
||||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList
|
||||
MuteList,
|
||||
EmojiInput
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
@ -159,8 +161,14 @@ const UserSettings = {
|
|||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper) {
|
||||
const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
|
||||
submitAvatar (cropper, file) {
|
||||
let img
|
||||
if (cropper) {
|
||||
img = cropper.getCroppedCanvas().toDataURL(file.type)
|
||||
} else {
|
||||
img = file
|
||||
}
|
||||
|
||||
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
|
|
|
@ -22,9 +22,18 @@
|
|||
<div class="setting-item" >
|
||||
<h2>{{$t('settings.name_bio')}}</h2>
|
||||
<p>{{$t('settings.name')}}</p>
|
||||
<input class='name-changer' id='username' v-model="newName"></input>
|
||||
<EmojiInput
|
||||
type="text"
|
||||
v-model="newName"
|
||||
id="username"
|
||||
classname="name-changer"
|
||||
/>
|
||||
<p>{{$t('settings.bio')}}</p>
|
||||
<textarea class="bio" v-model="newBio"></textarea>
|
||||
<EmojiInput
|
||||
type="textarea"
|
||||
v-model="newBio"
|
||||
classname="bio"
|
||||
/>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newLocked" id="account-locked">
|
||||
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
||||
|
@ -61,7 +70,7 @@
|
|||
<h2>{{$t('settings.avatar')}}</h2>
|
||||
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
||||
<p>{{$t('settings.current_avatar')}}</p>
|
||||
<img :src="user.profile_image_url_original" class="current-avatar"></img>
|
||||
<img :src="user.profile_image_url_original" class="current-avatar" />
|
||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
|
||||
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
|
||||
|
@ -69,12 +78,11 @@
|
|||
<div class="setting-item">
|
||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||
<p>{{$t('settings.current_profile_banner')}}</p>
|
||||
<img :src="user.cover_photo" class="banner"></img>
|
||||
<img :src="user.cover_photo" class="banner" />
|
||||
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
||||
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
|
||||
</img>
|
||||
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview" />
|
||||
<div>
|
||||
<input type="file" @change="uploadFile('banner', $event)" ></input>
|
||||
<input type="file" @change="uploadFile('banner', $event)" />
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
|
||||
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
|
||||
|
@ -86,10 +94,9 @@
|
|||
<div class="setting-item">
|
||||
<h2>{{$t('settings.profile_background')}}</h2>
|
||||
<p>{{$t('settings.set_new_profile_background')}}</p>
|
||||
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
|
||||
</img>
|
||||
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview" />
|
||||
<div>
|
||||
<input type="file" @change="uploadFile('background', $event)" ></input>
|
||||
<input type="file" @change="uploadFile('background', $event)" />
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
|
||||
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
|
||||
|
@ -165,7 +172,7 @@
|
|||
<h2>{{$t('settings.follow_import')}}</h2>
|
||||
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
|
||||
<form>
|
||||
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
||||
<input type="file" ref="followlist" v-on:change="followListChange" />
|
||||
</form>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
||||
|
@ -192,6 +199,12 @@
|
|||
<template slot="empty">{{$t('settings.no_blocks')}}</template>
|
||||
</block-list>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<mute-list :refresh="true">
|
||||
<template slot="empty">{{$t('settings.no_mutes')}}</template>
|
||||
</mute-list>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue