aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/attachment/attachment.vue1
-rw-r--r--src/components/basic_user_card/basic_user_card.js4
-rw-r--r--src/components/basic_user_card/basic_user_card.vue45
-rw-r--r--src/components/block_card/block_card.js2
-rw-r--r--src/components/conversation-page/conversation-page.js5
-rw-r--r--src/components/conversation-page/conversation-page.vue6
-rw-r--r--src/components/conversation/conversation.js99
-rw-r--r--src/components/conversation/conversation.vue50
-rw-r--r--src/components/emoji-input/emoji-input.js107
-rw-r--r--src/components/emoji-input/emoji-input.vue64
-rw-r--r--src/components/follow_card/follow_card.js7
-rw-r--r--src/components/follow_card/follow_card.vue27
-rw-r--r--src/components/gallery/gallery.vue1
-rw-r--r--src/components/image_cropper/image_cropper.js26
-rw-r--r--src/components/image_cropper/image_cropper.vue7
-rw-r--r--src/components/link-preview/link-preview.vue1
-rw-r--r--src/components/media_modal/media_modal.vue15
-rw-r--r--src/components/media_upload/media_upload.js2
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.js91
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.vue76
-rw-r--r--src/components/mute_card/mute_card.js2
-rw-r--r--src/components/mute_card/mute_card.vue16
-rw-r--r--src/components/notification/notification.js4
-rw-r--r--src/components/notification/notification.vue10
-rw-r--r--src/components/notifications/notifications.js3
-rw-r--r--src/components/notifications/notifications.scss6
-rw-r--r--src/components/post_status_form/post_status_form.js12
-rw-r--r--src/components/post_status_form/post_status_form.vue91
-rw-r--r--src/components/registration/registration.js3
-rw-r--r--src/components/registration/registration.vue2
-rw-r--r--src/components/remote_follow/remote_follow.js10
-rw-r--r--src/components/remote_follow/remote_follow.vue24
-rw-r--r--src/components/settings/settings.js31
-rw-r--r--src/components/settings/settings.vue40
-rw-r--r--src/components/side_drawer/side_drawer.js22
-rw-r--r--src/components/side_drawer/side_drawer.vue48
-rw-r--r--src/components/status/status.js13
-rw-r--r--src/components/status/status.vue28
-rw-r--r--src/components/status_or_conversation/status_or_conversation.js22
-rw-r--r--src/components/status_or_conversation/status_or_conversation.vue14
-rw-r--r--src/components/timeline/timeline.js8
-rw-r--r--src/components/timeline/timeline.vue8
-rw-r--r--src/components/user_avatar/user_avatar.js5
-rw-r--r--src/components/user_card/user_card.js (renamed from src/components/user_card_content/user_card_content.js)42
-rw-r--r--src/components/user_card/user_card.vue (renamed from src/components/user_card_content/user_card_content.vue)113
-rw-r--r--src/components/user_panel/user_panel.js4
-rw-r--r--src/components/user_panel/user_panel.vue12
-rw-r--r--src/components/user_profile/user_profile.js101
-rw-r--r--src/components/user_profile/user_profile.vue17
-rw-r--r--src/components/user_settings/user_settings.js14
-rw-r--r--src/components/user_settings/user_settings.vue35
51 files changed, 946 insertions, 450 deletions
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 76affe2d..c58bebd3 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -160,6 +160,7 @@
.hider {
position: absolute;
+ right: 0;
white-space: nowrap;
margin: 10px;
padding: 5px;
diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js
index a8441446..87085a28 100644
--- a/src/components/basic_user_card/basic_user_card.js
+++ b/src/components/basic_user_card/basic_user_card.js
@@ -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: {
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 77fb0aa0..8afe8b44 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -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>
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index 11fa27b4..c459ff1b 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -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
diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js
index 8f1ac3d9..1da70ce9 100644
--- a/src/components/conversation-page/conversation-page.js
+++ b/src/components/conversation-page/conversation-page.js
@@ -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
}
diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue
index b03eea28..9e322cf5 100644
--- a/src/components/conversation-page/conversation-page.vue
+++ b/src/components/conversation-page/conversation-page.vue
@@ -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>
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 48b8aaaa..69058bf6 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -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)
+ }
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 5528fef6..c39a3ed9 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -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>
diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js
new file mode 100644
index 00000000..a5bb6eaf
--- /dev/null
+++ b/src/components/emoji-input/emoji-input.js
@@ -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
diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue
new file mode 100644
index 00000000..338b77cd
--- /dev/null
+++ b/src/components/emoji-input/emoji-input.vue
@@ -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>
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index 425c9c3e..ac4e265a 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -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: {
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 6cb064eb..9f314fd3 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -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;
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index 2366ddf7..ea525c95 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -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;
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 990c0370..5ba8f04e 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -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 () {
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index 24a6f3bd..129e6f46 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -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>
diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue
index 7394668c..64b1a58b 100644
--- a/src/components/link-preview/link-preview.vue
+++ b/src/components/link-preview/link-preview.vue
@@ -23,6 +23,7 @@
flex-direction: row;
cursor: pointer;
overflow: hidden;
+ margin-top: 0.5em;
.card-image {
flex-shrink: 0;
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 427bf12b..7f666603 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -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;
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 1c874faa..e4b3d460 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -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
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.js b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
new file mode 100644
index 00000000..2f24dd08
--- /dev/null
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
@@ -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
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
new file mode 100644
index 00000000..0a451c28
--- /dev/null
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
@@ -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>
diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 5dd0a9e5..65c9cfb5 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -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
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index e1bfe20b..a4edff03 100644
--- a/src/components/mute_card/mute_card.vue
+++ b/src/components/mute_card/mute_card.vue
@@ -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> \ No newline at end of file
+<script src="./mute_card.js"></script>
+
+<style lang="scss">
+.mute-card-content-container {
+ margin-top: 0.5em;
+ text-align: right;
+ button {
+ width: 10em;
+ }
+}
+</style>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 7d9807de..fe5b7018 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -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 () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index a0a55cba..5e9cef97 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -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)">
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 5e95631a..9fc5e38a 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -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 {
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index b3364afc..c0b458cc 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -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 {
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index b0882f70..499cbbfb 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -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.
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 8beb73a9..3d3a1082 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -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>
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index ab6cd64d..8dc00420 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -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,
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index e22b308d..110b27bf 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -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 }">
diff --git a/src/components/remote_follow/remote_follow.js b/src/components/remote_follow/remote_follow.js
new file mode 100644
index 00000000..461d58c9
--- /dev/null
+++ b/src/components/remote_follow/remote_follow.js
@@ -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`
+ }
+ }
+}
diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue
new file mode 100644
index 00000000..fb2147bd
--- /dev/null
+++ b/src/components/remote_follow/remote_follow.vue
@@ -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>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 104be1a8..a85ab674 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -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 })
},
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index d9b72ee0..6ee103c7 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -37,6 +37,10 @@
<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">
{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
@@ -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>
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index b5c49059..567d2e5e 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -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)
}
}
}
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 6996380d..e5046496 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -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 {
diff --git a/src/components/status/status.js b/src/components/status/status.js
index fbbca6c4..550fe76f 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -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)
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 46919d7c..1f415534 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -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 {
diff --git a/src/components/status_or_conversation/status_or_conversation.js b/src/components/status_or_conversation/status_or_conversation.js
deleted file mode 100644
index 441552ca..00000000
--- a/src/components/status_or_conversation/status_or_conversation.js
+++ /dev/null
@@ -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
diff --git a/src/components/status_or_conversation/status_or_conversation.vue b/src/components/status_or_conversation/status_or_conversation.vue
deleted file mode 100644
index 9647d5eb..00000000
--- a/src/components/status_or_conversation/status_or_conversation.vue
+++ /dev/null
@@ -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>
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 655bfb3f..1da7d5cc 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -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)
) {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 8f28d65c..e0a34bd1 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -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">
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index e513b993..e6fed3b5 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -23,6 +23,11 @@ const UserAvatar = {
imageLoadError () {
this.showPlaceholder = true
}
+ },
+ watch: {
+ src () {
+ this.showPlaceholder = false
+ }
}
}
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card/user_card.js
index 7a7b89d4..197c61d5 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card/user_card.js
@@ -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) {
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card/user_card.vue
index 702c3385..3259d1c5 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card/user_card.vue
@@ -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%);
+ }
+
+ p {
+ margin-bottom: 0;
+ }
- .profile-bio {
+ &-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>
diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js
index 15804b88..d4478290 100644
--- a/src/components/user_panel/user_panel.js
+++ b/src/components/user_panel/user_panel.js
@@ -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
}
}
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 2d5cb500..8310f30e 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -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>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 7708141c..82df4510 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -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
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index a3d2825f..d449eb85 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -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;
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 1911ab23..4b277b6c 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -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])
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 7bd391ba..c08698dc 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -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>