aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/about/about.js3
-rw-r--r--src/components/about/about.vue2
-rw-r--r--src/components/autocomplete_input/autocomplete_input.js150
-rw-r--r--src/components/autocomplete_input/autocomplete_input.vue104
-rw-r--r--src/components/follow_list/follow_list.js63
-rw-r--r--src/components/follow_list/follow_list.vue34
-rw-r--r--src/components/media_upload/media_upload.js25
-rw-r--r--src/components/media_upload/media_upload.vue2
-rw-r--r--src/components/notification/notification.js4
-rw-r--r--src/components/notification/notification.vue2
-rw-r--r--src/components/notifications/notifications.scss21
-rw-r--r--src/components/post_status_form/post_status_form.js136
-rw-r--r--src/components/post_status_form/post_status_form.vue88
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.js2
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.vue2
-rw-r--r--src/components/public_timeline/public_timeline.js2
-rw-r--r--src/components/settings/settings.js16
-rw-r--r--src/components/settings/settings.vue36
-rw-r--r--src/components/side_drawer/side_drawer.js6
-rw-r--r--src/components/side_drawer/side_drawer.vue26
-rw-r--r--src/components/status/status.js42
-rw-r--r--src/components/status/status.vue79
-rw-r--r--src/components/still-image/still-image.js6
-rw-r--r--src/components/still-image/still-image.vue2
-rw-r--r--src/components/tab_switcher/tab_switcher.js2
-rw-r--r--src/components/tag_timeline/tag_timeline.js4
-rw-r--r--src/components/user_avatar/user_avatar.js29
-rw-r--r--src/components/user_avatar/user_avatar.vue42
-rw-r--r--src/components/user_card/user_card.js34
-rw-r--r--src/components/user_card/user_card.vue71
-rw-r--r--src/components/user_card_content/user_card_content.js77
-rw-r--r--src/components/user_card_content/user_card_content.vue26
-rw-r--r--src/components/user_profile/user_profile.js77
-rw-r--r--src/components/user_profile/user_profile.vue25
-rw-r--r--src/components/user_settings/user_settings.js26
-rw-r--r--src/components/user_settings/user_settings.vue25
36 files changed, 802 insertions, 489 deletions
diff --git a/src/components/about/about.js b/src/components/about/about.js
index b4433b4e..ae1cb182 100644
--- a/src/components/about/about.js
+++ b/src/components/about/about.js
@@ -7,6 +7,9 @@ const About = {
InstanceSpecificPanel,
FeaturesPanel,
TermsOfServicePanel
+ },
+ computed: {
+ showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
}
}
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index bf87e0b8..13dec87c 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,7 +1,7 @@
<template>
<div class="sidebar">
<instance-specific-panel></instance-specific-panel>
- <features-panel></features-panel>
+ <features-panel v-if="showFeaturesPanel"></features-panel>
<terms-of-service-panel></terms-of-service-panel>
</div>
</template>
diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js
new file mode 100644
index 00000000..2a959fd1
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.js
@@ -0,0 +1,150 @@
+import Completion from '../../services/completion/completion.js'
+import { take, filter, map } from 'lodash'
+
+const AutoCompleteInput = {
+ props: [
+ 'id',
+ 'classObj',
+ 'value',
+ 'placeholder',
+ 'autoResize',
+ 'multiline',
+ 'drop',
+ 'dragoverPrevent',
+ 'paste',
+ 'keydownMetaEnter',
+ 'keyupCtrlEnter'
+ ],
+ components: {},
+ mounted () {
+ this.autoResize && this.resize(this.$refs.textarea)
+ const textLength = this.$refs.textarea.value.length
+ this.$refs.textarea.setSelectionRange(textLength, textLength)
+ },
+ data () {
+ return {
+ caret: 0,
+ highlighted: 0,
+ text: this.value
+ }
+ },
+ computed: {
+ users () {
+ return this.$store.state.users.users
+ },
+ emoji () {
+ return this.$store.state.instance.emoji || []
+ },
+ customEmoji () {
+ return this.$store.state.instance.customEmoji || []
+ },
+ textAtCaret () {
+ return (this.wordAtCaret || {}).word || ''
+ },
+ wordAtCaret () {
+ const word = Completion.wordAtPosition(this.text, this.caret - 1) || {}
+ return word
+ },
+ candidates () {
+ const firstchar = this.textAtCaret.charAt(0)
+ if (firstchar === '@') {
+ const query = this.textAtCaret.slice(1).toUpperCase()
+ const matchedUsers = filter(this.users, (user) => {
+ return user.screen_name.toUpperCase().startsWith(query) ||
+ user.name && user.name.toUpperCase().startsWith(query)
+ })
+ if (matchedUsers.length <= 0) {
+ return false
+ }
+ // eslint-disable-next-line camelcase
+ return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
+ // eslint-disable-next-line camelcase
+ screen_name: `@${screen_name}`,
+ name: name,
+ img: profile_image_url_original,
+ highlighted: index === this.highlighted
+ }))
+ } else 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) => ({
+ screen_name: `:${shortcode}:`,
+ name: '',
+ utf: utf || '',
+ // eslint-disable-next-line camelcase
+ img: utf ? '' : this.$store.state.instance.server + image_url,
+ highlighted: index === this.highlighted
+ }))
+ } else {
+ return false
+ }
+ }
+ },
+ methods: {
+ setCaret ({target: {selectionStart}}) {
+ this.caret = selectionStart
+ },
+ cycleBackward (e) {
+ const len = this.candidates.length || 0
+ if (len > 0) {
+ e.preventDefault()
+ this.highlighted -= 1
+ if (this.highlighted < 0) {
+ this.highlighted = this.candidates.length - 1
+ }
+ } else {
+ this.highlighted = 0
+ }
+ },
+ cycleForward (e) {
+ const len = this.candidates.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
+ }
+ },
+ replace (replacement) {
+ this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement)
+ const el = this.$el.querySelector('textarea')
+ el.focus()
+ this.caret = 0
+ },
+ replaceCandidate (e) {
+ const len = this.candidates.length || 0
+ if (this.textAtCaret === ':' || e.ctrlKey) { return }
+ if (len > 0) {
+ e.preventDefault()
+ const candidate = this.candidates[this.highlighted]
+ const replacement = candidate.utf || (candidate.screen_name + ' ')
+ this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement)
+ const el = this.$el.querySelector('textarea') || this.$el.querySelector('input')
+ el.focus()
+ this.caret = 0
+ this.highlighted = 0
+ }
+ },
+ resize (e) {
+ const target = e.target || e
+ if (!(target instanceof window.Element)) { return }
+ const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
+ Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
+ // Auto is needed to make textbox shrink when removing lines
+ target.style.height = 'auto'
+ target.style.height = `${target.scrollHeight - vertPadding}px`
+ if (target.value === '') {
+ target.style.height = null
+ }
+ }
+ }
+}
+
+export default AutoCompleteInput
diff --git a/src/components/autocomplete_input/autocomplete_input.vue b/src/components/autocomplete_input/autocomplete_input.vue
new file mode 100644
index 00000000..56233535
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.vue
@@ -0,0 +1,104 @@
+<template>
+ <div style="display: flex; flex-direction: column;">
+ <textarea
+ v-if="multiline"
+ ref="textarea"
+ rows="1"
+ :value="text" :class="classObj" :id="id" :placeholder="placeholder"
+ @input="text = $event.target.value, $emit('input', $event.target.value), autoResize && resize($event)"
+ @click="setCaret"
+ @keyup="setCaret"
+ @keydown.down="cycleForward"
+ @keydown.up="cycleBackward"
+ @keydown.shift.tab="cycleBackward"
+ @keydown.tab="cycleForward"
+ @keydown.enter="replaceCandidate"
+ @drop="drop && drop()"
+ @dragover.prevent="dragoverPrevent && dragoverPrevent()"
+ @paste="paste && paste()"
+ @keydown.meta.enter="keydownMetaEnter && keydownMetaEnter()"
+ @keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter()">
+ </textarea>
+ <input
+ v-else
+ ref="textarea"
+ :value="text" :class="classObj" :id="id" :placeholder="placeholder"
+ @input="text = $event.target.value, $emit('input', $event.target.value), autoResize && resize($event)"
+ @click="setCaret"
+ @keyup="setCaret"
+ @keydown.down="cycleForward"
+ @keydown.up="cycleBackward"
+ @keydown.shift.tab="cycleBackward"
+ @keydown.tab="cycleForward"
+ @keydown.enter="replaceCandidate"
+ @drop="drop && drop()"
+ @dragover.prevent="dragoverPrevent && dragoverPrevent()"
+ @paste="paste && paste()"
+ @keydown.meta.enter="keydownMetaEnter && keydownMetaEnter()"
+ @keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter()"/>
+ <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>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./autocomplete_input.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.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/follow_list/follow_list.js b/src/components/follow_list/follow_list.js
new file mode 100644
index 00000000..acdb216d
--- /dev/null
+++ b/src/components/follow_list/follow_list.js
@@ -0,0 +1,63 @@
+import UserCard from '../user_card/user_card.vue'
+
+const FollowList = {
+ data () {
+ return {
+ loading: false,
+ bottomedOut: false,
+ error: false
+ }
+ },
+ props: ['userId', 'showFollowers'],
+ created () {
+ window.addEventListener('scroll', this.scrollLoad)
+ if (this.entries.length === 0) {
+ this.fetchEntries()
+ }
+ },
+ destroyed () {
+ window.removeEventListener('scroll', this.scrollLoad)
+ this.$store.dispatch('clearFriendsAndFollowers', this.userId)
+ },
+ computed: {
+ user () {
+ return this.$store.getters.userById(this.userId)
+ },
+ entries () {
+ return this.showFollowers ? this.user.followers : this.user.friends
+ },
+ showActions () { return this.$store.state.users.currentUser.id === this.userId }
+ },
+ methods: {
+ fetchEntries () {
+ if (!this.loading) {
+ const command = this.showFollowers ? 'addFollowers' : 'addFriends'
+ this.loading = true
+ this.$store.dispatch(command, this.userId).then(entries => {
+ this.error = false
+ this.loading = false
+ this.bottomedOut = entries.length === 0
+ }).catch(() => {
+ this.error = true
+ this.loading = false
+ })
+ }
+ },
+ scrollLoad (e) {
+ const bodyBRect = document.body.getBoundingClientRect()
+ const height = Math.max(bodyBRect.height, -(bodyBRect.y))
+ if (this.loading === false &&
+ this.bottomedOut === false &&
+ this.$el.offsetHeight > 0 &&
+ (window.innerHeight + window.pageYOffset) >= (height - 750)
+ ) {
+ this.fetchEntries()
+ }
+ }
+ },
+ components: {
+ UserCard
+ }
+}
+
+export default FollowList
diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue
new file mode 100644
index 00000000..7be2e7b7
--- /dev/null
+++ b/src/components/follow_list/follow_list.vue
@@ -0,0 +1,34 @@
+<template>
+ <div class="follow-list">
+ <user-card
+ v-for="entry in entries"
+ :key="entry.id" :user="entry"
+ :showFollows="!showFollowers"
+ :showActions="showActions"
+ />
+ <div class="text-center panel-footer">
+ <a v-if="error" @click="fetchEntries" class="alert error">
+ {{$t('general.generic_error')}}
+ </a>
+ <i v-else-if="loading" class="icon-spin3 animate-spin"/>
+ <span v-else-if="bottomedOut"></span>
+ <a v-else @click="fetchEntries">{{$t('general.more')}}</a>
+ </div>
+ </div>
+</template>
+
+<script src="./follow_list.js"></script>
+
+<style lang="scss">
+
+.follow-list {
+ .panel-footer {
+ padding: 10px;
+ }
+
+ .error {
+ font-size: 14px;
+ }
+}
+
+</style>
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 42d900d3..1c874faa 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -3,19 +3,10 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const mediaUpload = {
- mounted () {
- const input = this.$el.querySelector('input')
-
- input.addEventListener('change', ({target}) => {
- for (var i = 0; i < target.files.length; i++) {
- let file = target.files[i]
- this.uploadFile(file)
- }
- })
- },
data () {
return {
- uploading: false
+ uploading: false,
+ uploadReady: true
}
},
methods: {
@@ -56,6 +47,18 @@ const mediaUpload = {
} else {
e.dataTransfer.dropEffect = 'none'
}
+ },
+ clearFile () {
+ this.uploadReady = false
+ this.$nextTick(() => {
+ this.uploadReady = true
+ })
+ },
+ change ({target}) {
+ for (var i = 0; i < target.files.length; i++) {
+ let file = target.files[i]
+ this.uploadFile(file)
+ }
}
},
props: [
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 768d3565..fcdc3471 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -3,7 +3,7 @@
<label class="btn btn-default" :title="$t('tool_tip.media_upload')">
<i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" v-if="!uploading"></i>
- <input type="file" style="position: fixed; top: -100em" multiple="true"></input>
+ <input type="file" v-if="uploadReady" @change="change" style="position: fixed; top: -100em" multiple="true"></input>
</label>
</div>
</template>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index f95a329f..7d9807de 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,5 +1,5 @@
import Status from '../status/status.vue'
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCardContent from '../user_card_content/user_card_content.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, StillImage, UserCardContent
+ Status, UserAvatar, UserCardContent
},
methods: {
toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index f91c90cc..a0a55cba 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -2,7 +2,7 @@
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
- <StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
+ <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">
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 5c4ca1b9..bc81d45c 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -36,26 +36,7 @@
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- .avatar-compact {
- width: 32px;
- height: 32px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
- overflow: hidden;
- line-height: 0;
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-
- &.animated::before {
- display: none;
- }
- }
-
- &:hover .animated.avatar-compact {
+ &:hover .animated.avatar {
canvas {
display: none;
}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 88bc736f..8e30264d 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -1,8 +1,8 @@
import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
+import AutoCompleteInput from '../autocomplete_input/autocomplete_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'
+import { reject, map, uniqBy } from 'lodash'
const buildMentionsString = ({user, attentions}, currentUser) => {
let allAttentions = [...attentions]
@@ -28,13 +28,10 @@ const PostStatusForm = {
'subject'
],
components: {
- MediaUpload
+ MediaUpload,
+ AutoCompleteInput
},
mounted () {
- this.resize(this.$refs.textarea)
- const textLength = this.$refs.textarea.value.length
- this.$refs.textarea.setSelectionRange(textLength, textLength)
-
if (this.replyTo) {
this.$refs.textarea.focus()
}
@@ -61,16 +58,13 @@ const PostStatusForm = {
submitDisabled: false,
error: null,
posting: false,
- highlighted: 0,
newStatus: {
spoilerText: this.subject || '',
status: statusText,
- contentType: 'text/plain',
nsfw: false,
files: [],
visibility: scope
- },
- caret: 0
+ }
}
},
computed: {
@@ -82,59 +76,6 @@ const PostStatusForm = {
direct: { selected: this.newStatus.visibility === 'direct' }
}
},
- candidates () {
- const firstchar = this.textAtCaret.charAt(0)
- if (firstchar === '@') {
- const query = this.textAtCaret.slice(1).toUpperCase()
- const matchedUsers = filter(this.users, (user) => {
- return user.screen_name.toUpperCase().startsWith(query) ||
- user.name && user.name.toUpperCase().startsWith(query)
- })
- if (matchedUsers.length <= 0) {
- return false
- }
- // eslint-disable-next-line camelcase
- return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
- // eslint-disable-next-line camelcase
- screen_name: `@${screen_name}`,
- name: name,
- img: profile_image_url_original,
- highlighted: index === this.highlighted
- }))
- } else 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) => ({
- screen_name: `:${shortcode}:`,
- name: '',
- 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.newStatus.status, this.caret - 1) || {}
- return word
- },
- users () {
- return this.$store.state.users.users
- },
- emoji () {
- return this.$store.state.instance.emoji || []
- },
- customEmoji () {
- return this.$store.state.instance.customEmoji || []
- },
statusLength () {
return this.newStatus.status.length
},
@@ -167,56 +108,16 @@ const PostStatusForm = {
},
formattingOptionsEnabled () {
return this.$store.state.instance.formattingOptionsEnabled
+ },
+ defaultPostContentType () {
+ return typeof this.$store.state.config.postContentType === 'undefined'
+ ? this.$store.state.instance.postContentType
+ : this.$store.state.config.postContentType
}
},
methods: {
- replace (replacement) {
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- },
- replaceCandidate (e) {
- const len = this.candidates.length || 0
- if (this.textAtCaret === ':' || e.ctrlKey) { return }
- if (len > 0) {
- e.preventDefault()
- const candidate = this.candidates[this.highlighted]
- const replacement = candidate.utf || (candidate.screen_name + ' ')
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- this.highlighted = 0
- }
- },
- cycleBackward (e) {
- const len = this.candidates.length || 0
- if (len > 0) {
- e.preventDefault()
- this.highlighted -= 1
- if (this.highlighted < 0) {
- this.highlighted = this.candidates.length - 1
- }
- } else {
- this.highlighted = 0
- }
- },
- cycleForward (e) {
- const len = this.candidates.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
- }
- },
- setCaret ({target: {selectionStart}}) {
- this.caret = selectionStart
+ postStatusCopy () {
+ this.postStatus(this.newStatus)
},
postStatus (newStatus) {
if (this.posting) { return }
@@ -250,6 +151,7 @@ const PostStatusForm = {
visibility: newStatus.visibility,
contentType: newStatus.contentType
}
+ this.$refs.mediaUpload.clearFile()
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
@@ -300,18 +202,6 @@ const PostStatusForm = {
fileDrag (e) {
e.dataTransfer.dropEffect = 'copy'
},
- resize (e) {
- const target = e.target || e
- if (!(target instanceof window.Element)) { return }
- const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
- Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
- // Auto is needed to make textbox shrink when removing lines
- target.style.height = 'auto'
- target.style.height = `${target.scrollHeight - vertPadding}px`
- if (target.value === '') {
- target.style.height = null
- }
- },
clearError () {
this.error = null
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 398f1871..ef3a7901 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -16,26 +16,20 @@
:placeholder="$t('post_status.content_warning')"
v-model="newStatus.spoilerText"
class="form-cw">
- <textarea
- ref="textarea"
- @click="setCaret"
- @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
- @keydown.down="cycleForward"
- @keydown.up="cycleBackward"
- @keydown.shift.tab="cycleBackward"
- @keydown.tab="cycleForward"
- @keydown.enter="replaceCandidate"
- @keydown.meta.enter="postStatus(newStatus)"
- @keyup.ctrl.enter="postStatus(newStatus)"
- @drop="fileDrop"
- @dragover.prevent="fileDrag"
- @input="resize"
- @paste="paste">
- </textarea>
+ <auto-complete-input v-model="newStatus.status"
+ :classObj="{ 'form-control': true }"
+ :placeholder="$t('post_status.default')"
+ :autoResize="true"
+ :multiline="true"
+ :drop="fileDrop"
+ :dragoverPrevent="fileDrag"
+ :paste="paste"
+ :keydownMetaEnter="postStatusCopy"
+ :keyupCtrlEnter="postStatusCopy"/>
<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">
+ <select id="post-content-type" v-model="defaultPostContentType" 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>
@@ -52,19 +46,8 @@
</div>
</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>
- </div>
- </div>
<div class='form-bottom'>
- <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
+ <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
@@ -250,52 +233,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/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js
index 0db6efae..d45677e0 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.js
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.js
@@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
},
created () {
- this.$store.dispatch('startFetching', 'publicAndExternal')
+ this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal')
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.vue b/src/components/public_and_external_timeline/public_and_external_timeline.vue
index aded2ead..6be9f955 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.vue
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.vue
@@ -1,5 +1,5 @@
<template>
- <Timeline :title="$t('nav.twkn')"v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
+ <Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
</template>
<script src="./public_and_external_timeline.js"></script>
diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js
index 9b866be8..64c951ac 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -7,7 +7,7 @@ const PublicTimeline = {
timeline () { return this.$store.state.statuses.timelines.public }
},
created () {
- this.$store.dispatch('startFetching', 'public')
+ this.$store.dispatch('startFetching', { timeline: 'public' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'public')
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 8d138485..534a9839 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -27,6 +27,11 @@ const settings = {
: user.hideUserStats,
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
+ hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
+ ? instance.hideFilteredStatuses
+ : user.hideFilteredStatuses,
+ hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
+
notificationVisibilityLocal: user.notificationVisibility,
replyVisibilityLocal: user.replyVisibility,
loopVideoLocal: user.loopVideo,
@@ -46,6 +51,11 @@ const settings = {
: user.subjectLineBehavior,
subjectLineBehaviorDefault: instance.subjectLineBehavior,
+ postContentTypeLocal: typeof user.postContentType === 'undefined'
+ ? instance.postContentType
+ : user.postContentType,
+ postContentTypeDefault: instance.postContentType,
+
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
? instance.alwaysShowSubjectInput
: user.alwaysShowSubjectInput,
@@ -96,6 +106,9 @@ const settings = {
hideUserStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
},
+ hideFilteredStatusesLocal (value) {
+ this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
+ },
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
@@ -157,6 +170,9 @@ const settings = {
subjectLineBehaviorLocal (value) {
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
},
+ postContentTypeLocal (value) {
+ this.$store.dispatch('setOption', { name: 'postContentType', value })
+ },
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
},
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 9953780f..dfb2e49d 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -100,6 +100,28 @@
</label>
</div>
</li>
+ <li>
+ <div>
+ {{$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>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ </li>
</ul>
</div>
@@ -205,7 +227,6 @@
</label>
</li>
</ul>
- </label>
</div>
<div>
{{$t('settings.replies_in_timeline')}}
@@ -232,11 +253,18 @@
</div>
</div>
<div class="setting-item">
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ <div>
+ <p>{{$t('settings.filtering_explanation')}}</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ <div>
+ <input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal">
+ <label for="hideFilteredStatuses">
+ {{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}}
+ </label>
+ </div>
</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 754a57e0..40ffa1dd 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -26,6 +26,12 @@ const SideDrawer = {
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
+ },
+ logo () {
+ return this.$store.state.instance.logo
+ },
+ sitename () {
+ return this.$store.state.instance.name
}
},
methods: {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index fc90977b..a6c6f237 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -8,8 +8,11 @@
@touchmove="touchMove"
>
<div class="side-drawer-heading" @click="toggleDrawer">
- <user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
- </user-card-content>
+ <user-card-content :user="currentUser" :switcher="false" :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">
@@ -141,6 +144,24 @@
background-color: var(--bg, $fallback--bg);
}
+.side-drawer-logo-wrapper {
+ display: flex;
+ align-items: center;
+ padding: 0.85em;
+
+ img {
+ flex: none;
+ height: 50px;
+ margin-right: 0.85em;
+ }
+
+ span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
.side-drawer-click-outside-closed {
flex: 0 0 0;
}
@@ -154,7 +175,6 @@
flex-direction: column;
align-items: stretch;
display: flex;
- min-height: 7em;
padding: 0;
margin: 0;
diff --git a/src/components/status/status.js b/src/components/status/status.js
index c718fe9f..0273a5be 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -4,14 +4,14 @@ 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 StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
-import { filter, find } from 'lodash'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { filter, find, unescape } from 'lodash'
const Status = {
name: 'Status',
@@ -36,6 +36,7 @@ const Status = {
preview: null,
showPreview: false,
showingTall: this.inConversation && this.focused,
+ showingLongSubject: false,
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
@@ -89,6 +90,7 @@ const Status = {
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },
retweeterHtml () { return this.statusoid.user.name_html },
+ retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
@@ -108,6 +110,14 @@ const Status = {
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
+ hideFilteredStatuses () {
+ return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
+ ? this.$store.state.instance.hideFilteredStatuses
+ : this.$store.state.config.hideFilteredStatuses
+ },
+ hideStatus () {
+ return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
+ },
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
@@ -129,6 +139,9 @@ const Status = {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
+ longSubject () {
+ return this.status.summary.length > 900
+ },
isReply () {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
@@ -196,14 +209,15 @@ const Status = {
},
replySubject () {
if (!this.status.summary) return ''
+ const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
- const startsWithRe = this.status.summary.match(/^re[: ]/i)
+ const startsWithRe = decodedSummary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
- return this.status.summary
+ return decodedSummary
} else if (behavior === 'email') {
- return 're: '.concat(this.status.summary)
+ return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
@@ -244,7 +258,7 @@ const Status = {
DeleteButton,
PostStatusForm,
UserCardContent,
- StillImage,
+ UserAvatar,
Gallery,
LinkPreview
},
@@ -268,7 +282,7 @@ const Status = {
}
if (target.tagName === 'A') {
if (target.className.match(/mention/)) {
- const href = target.getAttribute('href')
+ const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
@@ -278,6 +292,15 @@ const Status = {
return
}
}
+ if (target.className.match(/hashtag/)) {
+ // Extract tag name from link url
+ const tag = extractTagFromUrl(target.href)
+ if (tag) {
+ const link = this.generateTagLink(tag)
+ this.$router.push(link)
+ return
+ }
+ }
window.open(target.href, '_blank')
}
},
@@ -334,6 +357,9 @@ const Status = {
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
+ generateTagLink (tag) {
+ return `/tag/${tag}`
+ },
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3e3e82bf..aae365d1 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,5 +1,5 @@
<template>
- <div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
+ <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small>
@@ -13,10 +13,12 @@
</template>
<template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
- <StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
+ <UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
- <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
- <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
+ <span class="user-name">
+ <router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
+ <router-link v-else :to="retweeterProfileLink">{{retweeter}}</router-link>
+ </span>
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
{{$t('timeline.repeated')}}
</div>
@@ -25,7 +27,7 @@
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
- <StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/>
+ <UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
</router-link>
</div>
<div class="status-body">
@@ -54,7 +56,7 @@
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
- <small class="reply-link" v-for="reply in replies">
+ <small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
@@ -85,7 +87,12 @@
</div>
</div>
- <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
+ <div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
+ <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">Show more</a>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
+ <a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">Show less</a>
+ </div>
+ <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
@@ -93,7 +100,7 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
- <div v-if="status.attachments && !hideSubjectStatus" class="attachments media-body">
+ <div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
<attachment
class="non-gallery"
v-for="attachment in nonGalleryAttachments"
@@ -413,7 +420,7 @@
padding: 0.4em 0.6em 0 0.6em;
margin: 0;
- .avatar {
+ .avatar.still-image {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
@@ -431,6 +438,8 @@
.user-name {
font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
img {
width: 14px;
@@ -497,46 +506,6 @@
color: var(--cBlue, $fallback--cBlue);
}
-.status .avatar-compact {
- width: 32px;
- height: 32px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-}
-
-.avatar.still-image {
- width: 48px;
- height: 48px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- overflow: hidden;
- position: relative;
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-
- img {
- width: 100%;
- height: 100%;
- }
-
- &.animated::before {
- display: none;
- }
-
- &.retweeted {
- }
-}
-
.status:hover .animated.avatar {
canvas {
display: none;
@@ -594,7 +563,7 @@ a.unmute {
@media all and (max-width: 800px) {
.status-el {
.retweet-info {
- .avatar {
+ .avatar.still-image {
margin-left: 20px;
}
}
@@ -603,14 +572,14 @@ a.unmute {
max-width: 100%;
}
- .status .avatar {
+ .status .avatar.still-image {
width: 40px;
height: 40px;
- }
- .status .avatar-compact {
- width: 32px;
- height: 32px;
+ &.avatar-compact {
+ width: 32px;
+ height: 32px;
+ }
}
}
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index 5ad06dc2..02e98f19 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -2,7 +2,8 @@ const StillImage = {
props: [
'src',
'referrerpolicy',
- 'mimetype'
+ 'mimetype',
+ 'imageLoadError'
],
data () {
return {
@@ -23,6 +24,9 @@ const StillImage = {
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
+ },
+ onError () {
+ this.imageLoadError && this.imageLoadError()
}
}
}
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index 1dcb7ce6..af824fa2 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -1,7 +1,7 @@
<template>
<div class='still-image' :class='{ animated: animated }' >
<canvas ref="canvas" v-if="animated"></canvas>
- <img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad"/>
+ <img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad" @error="onError"/>
</div>
</template>
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index f9c3f927..423df258 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', {
return (
<div class={ classesWrapper.join(' ')}>
- <button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
+ <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
</div>
)
})
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 43de4f49..41b09706 100644
--- a/src/components/tag_timeline/tag_timeline.js
+++ b/src/components/tag_timeline/tag_timeline.js
@@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
const TagTimeline = {
created () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { 'tag': this.tag })
+ this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
},
components: {
Timeline
@@ -15,7 +15,7 @@ const TagTimeline = {
watch: {
tag () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { 'tag': this.tag })
+ this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
}
},
destroyed () {
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
new file mode 100644
index 00000000..e513b993
--- /dev/null
+++ b/src/components/user_avatar/user_avatar.js
@@ -0,0 +1,29 @@
+import StillImage from '../still-image/still-image.vue'
+
+const UserAvatar = {
+ props: [
+ 'src',
+ 'betterShadow',
+ 'compact'
+ ],
+ data () {
+ return {
+ showPlaceholder: false
+ }
+ },
+ components: {
+ StillImage
+ },
+ computed: {
+ imgSrc () {
+ return this.showPlaceholder ? '/images/avi.png' : this.src
+ }
+ },
+ methods: {
+ imageLoadError () {
+ this.showPlaceholder = true
+ }
+ }
+}
+
+export default UserAvatar
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
new file mode 100644
index 00000000..6bf7123d
--- /dev/null
+++ b/src/components/user_avatar/user_avatar.vue
@@ -0,0 +1,42 @@
+<template>
+ <StillImage
+ class="avatar"
+ :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
+ :src="imgSrc"
+ :imageLoadError="imageLoadError"
+ />
+</template>
+
+<script src="./user_avatar.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.avatar.still-image {
+ width: 48px;
+ height: 48px;
+ box-shadow: var(--avatarStatusShadow);
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+
+ &.better-shadow {
+ box-shadow: var(--avatarStatusShadowInset);
+ filter: var(--avatarStatusShadowFilter)
+ }
+
+ &.animated::before {
+ display: none;
+ }
+
+ &.avatar-compact {
+ width: 32px;
+ height: 32px;
+ border-radius: $fallback--avatarAltRadius;
+ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ }
+}
+</style>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 615e6487..a4c84716 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,24 +1,33 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const UserCard = {
props: [
'user',
'showFollows',
- 'showApproval'
+ 'showApproval',
+ 'showActions'
],
data () {
return {
- userExpanded: false
+ userExpanded: false,
+ followRequestInProgress: false,
+ followRequestSent: false,
+ updated: false
}
},
components: {
UserCardContent,
- StillImage
+ UserAvatar
},
computed: {
- currentUser () { return this.$store.state.users.currentUser }
+ currentUser () { return this.$store.state.users.currentUser },
+ following () { return this.updated ? this.updated.following : this.user.following },
+ showFollow () {
+ return this.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following)
+ }
},
methods: {
toggleUserExpanded () {
@@ -34,6 +43,21 @@ const UserCard = {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ followUser () {
+ this.followRequestInProgress = true
+ requestFollow(this.user, this.$store).then(({ sent, updated }) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ this.updated = updated
+ })
+ },
+ unfollowUser () {
+ this.followRequestInProgress = true
+ requestUnfollow(this.user, this.$store).then(({ updated }) => {
+ this.followRequestInProgress = false
+ this.updated = updated
+ })
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index cf69606d..12960c02 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,28 +1,49 @@
<template>
<div class="card">
- <a href="#">
- <StillImage @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/>
- </a>
+ <router-link :to="userProfileLink(user)">
+ <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
+ </router-link>
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
- <div :title="user.name" v-if="user.name_html" class="user-name">
- <span v-html="user.name_html"></span>
+ <div :title="user.name" class="user-name">
+ <span v-if="user.name_html" v-html="user.name_html"></span>
+ <span v-else>{{ user.name }}</span>
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
</div>
- <div :title="user.name" v-else class="user-name">
- {{ user.name }}
- <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
- {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
- </span>
+ <div class="user-link-action">
+ <router-link class='user-screen-name' :to="userProfileLink(user)">
+ @{{user.screen_name}}
+ </router-link>
+ <button
+ v-if="showFollow"
+ class="btn btn-default"
+ @click="followUser"
+ :disabled="followRequestInProgress"
+ :title="followRequestSent ? $t('user_card.follow_again') : ''"
+ >
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else-if="followRequestSent">
+ {{ $t('user_card.follow_sent') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow') }}
+ </template>
+ </button>
+ <button v-if="showActions && showFollows && following" class="btn btn-default" @click="unfollowUser" :disabled="followRequestInProgress">
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow_unfollow') }}
+ </template>
+ </button>
</div>
-
- <router-link class='user-screen-name' :to="userProfileLink(user)">
- @{{user.screen_name}}
- </router-link>
</div>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
@@ -42,6 +63,9 @@
text-align: left;
width: 100%;
.user-name {
+ display: flex;
+ justify-content: space-between;
+
img {
object-fit: contain;
height: 16px;
@@ -49,11 +73,20 @@
vertical-align: middle;
}
}
+
+ .user-link-action {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ button {
+ margin-top: 3px;
+ }
+ }
}
.follows-you {
margin-left: 2em;
- float: right;
}
.card {
@@ -69,17 +102,13 @@
border-bottom-color: var(--border, $fallback--border);
.avatar {
- margin-top: 0.2em;
- width:32px;
- height: 32px;
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ padding: 0;
}
}
.usercard {
width: fill-available;
- margin: 0.2em 0 0.7em 0;
+ margin: 0.2em 0 0 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 541c73b4..7a7b89d4 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -1,5 +1,6 @@
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.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 {
@@ -79,76 +80,30 @@ export default {
set (color) {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
}
+ },
+ visibleRole () {
+ const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
+ const showRole = this.isOtherUser || this.user.show_role
+
+ return validRole && showRole && this.user.role
}
},
components: {
- StillImage
+ UserAvatar
},
methods: {
followUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.followUser(this.user.id)
- .then((followedUser) => store.commit('addNewUsers', [followedUser]))
- .then(() => {
- // For locked users we just mark it that we sent the follow request
- if (this.user.locked) {
- this.followRequestInProgress = false
- this.followRequestSent = true
- return
- }
-
- if (this.user.following) {
- // If we get result immediately, just stop.
- this.followRequestInProgress = false
- return
- }
-
- // But usually we don't get result immediately, so we ask server
- // for updated user profile to confirm if we are following them
- // Sometimes it takes several tries. Sometimes we end up not following
- // user anyway, probably because they locked themselves and we
- // don't know that yet.
- // Recursive Promise, it will call itself up to 3 times.
- const fetchUser = (attempt) => new Promise((resolve, reject) => {
- setTimeout(() => {
- store.state.api.backendInteractor.fetchUser({ id: this.user.id })
- .then((user) => store.commit('addNewUsers', [user]))
- .then(() => resolve([this.user.following, attempt]))
- .catch((e) => reject(e))
- }, 500)
- }).then(([following, attempt]) => {
- if (!following && attempt <= 3) {
- // If we BE reports that we still not following that user - retry,
- // increment attempts by one
- return fetchUser(++attempt)
- } else {
- // If we run out of attempts, just return whatever status is.
- return following
- }
- })
-
- return fetchUser(1)
- .then((following) => {
- if (following) {
- // We confirmed and everything its good.
- this.followRequestInProgress = false
- } else {
- // If after all the tries, just treat it as if user is locked
- this.followRequestInProgress = false
- this.followRequestSent = true
- }
- })
- })
+ requestFollow(this.user, this.$store).then(({sent}) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ })
},
unfollowUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.unfollowUser(this.user.id)
- .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
- .then(() => {
- this.followRequestInProgress = false
- })
+ requestUnfollow(this.user, this.$store).then(() => {
+ this.followRequestInProgress = false
+ })
},
blockUser () {
const store = this.$store
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index d1034d68..7f9909c4 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -4,7 +4,7 @@
<div class='user-info'>
<div class='container'>
<router-link :to="userProfileLink(user)">
- <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
+ <UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
</router-link>
<div class="name-and-screen-name">
<div class="top-line">
@@ -19,7 +19,9 @@
</div>
<router-link class='user-screen-name' :to="userProfileLink(user)">
- <span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
+ <span class="handle">@{{user.screen_name}}
+ <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
+ </span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
</div>
@@ -169,23 +171,12 @@
max-height: 56px;
.avatar {
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
flex: 1 0 100%;
width: 56px;
height: 56px;
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
box-shadow: var(--avatarShadow);
object-fit: cover;
-
- &.better-shadow {
- box-shadow: var(--avatarShadowInset);
- filter: var(--avatarShadowFilter)
- }
-
- &.animated::before {
- display: none;
- }
}
}
@@ -258,6 +249,15 @@
text-overflow: ellipsis;
overflow: hidden;
}
+
+ // TODO use proper colors
+ .staff {
+ text-transform: capitalize;
+ color: $fallback--text;
+ color: var(--btnText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btn, $fallback--fg);
+ }
}
.user-meta {
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 27e138b0..a22b8722 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,23 +1,22 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import Timeline from '../timeline/timeline.vue'
+import FollowList from '../follow_list/follow_list.vue'
const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
+ 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)
}
},
destroyed () {
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
+ this.cleanUp(this.userId)
},
computed: {
timeline () {
@@ -39,12 +38,6 @@ const UserProfile = {
return this.userId && this.$store.state.users.currentUser.id &&
this.userId === this.$store.state.users.currentUser.id
},
- friends () {
- return this.user.friends
- },
- followers () {
- return this.user.followers
- },
userInStore () {
if (this.isExternal) {
return this.$store.getters.userById(this.userId)
@@ -65,64 +58,56 @@ const UserProfile = {
},
isExternal () {
return this.$route.name === 'external-user-profile'
+ },
+ followsTabVisible () {
+ return this.isUs || !this.user.hide_follows
+ },
+ followersTabVisible () {
+ return this.isUs || !this.user.hide_followers
}
},
methods: {
- fetchFollowers () {
- const id = this.userId
- this.$store.dispatch('addFollowers', { id })
- },
- fetchFriends () {
- const id = this.userId
- this.$store.dispatch('addFriends', { id })
- },
startFetchFavorites () {
if (this.isUs) {
- this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
+ this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
}
+ },
+ startUp () {
+ this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
+ this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
+
+ this.startFetchFavorites()
+ },
+ cleanUp () {
+ this.$store.dispatch('stopFetching', 'user')
+ this.$store.dispatch('stopFetching', 'favorites')
+ this.$store.dispatch('stopFetching', 'media')
+ this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.commit('clearTimeline', { timeline: 'favorites' })
+ this.$store.commit('clearTimeline', { timeline: 'media' })
}
},
watch: {
- // TODO get rid of this copypasta
userName () {
if (this.isExternal) {
return
}
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
- this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.commit('clearTimeline', { timeline: 'favorites' })
- this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
- this.startFetchFavorites()
+ this.cleanUp()
+ this.startUp()
},
userId () {
if (!this.isExternal) {
return
}
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
- this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.commit('clearTimeline', { timeline: 'favorites' })
- this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
- this.startFetchFavorites()
- },
- user () {
- if (this.user.id && !this.user.followers) {
- this.fetchFollowers()
- this.fetchFriends()
- }
+ this.cleanUp()
+ this.startUp()
}
},
components: {
UserCardContent,
UserCard,
- Timeline
+ Timeline,
+ FollowList
}
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index e53ce4cc..79461291 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -9,40 +9,28 @@
<tab-switcher :renderOnlyFocused="true">
<Timeline
:label="$t('user_card.statuses')"
+ :disabled="!user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
:timeline-name="'user'"
:user-id="fetchBy"
/>
- <div :label="$t('user_card.followees')">
- <div v-if="friends">
- <user-card
- v-for="friend in friends"
- :key="friend.id"
- :user="friend"
- :showFollows="true"
- />
- </div>
+ <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
+ <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
- <div :label="$t('user_card.followers')">
- <div v-if="followers">
- <user-card
- v-for="follower in followers"
- :key="follower.id"
- :user="follower"
- :showFollows="false"
- />
- </div>
+ <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
+ <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
<Timeline
:label="$t('user_card.media')"
+ :disabled="!media.visibleStatuses.length"
:embedded="true" :title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
@@ -51,6 +39,7 @@
<Timeline
v-if="isUs"
:label="$t('user_card.favorites')"
+ :disabled="!favorites.visibleStatuses.length"
:embedded="true"
:title="$t('user_card.favorites')"
timeline-name="favorites"
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 9bd8aa00..fa389c3b 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,16 +1,22 @@
+import { unescape } from 'lodash'
+
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
+import AutoCompleteInput from '../autocomplete_input/autocomplete_input.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const UserSettings = {
data () {
return {
newName: this.$store.state.users.currentUser.name,
- newBio: this.$store.state.users.currentUser.description,
+ newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope,
- newHideNetwork: this.$store.state.users.currentUser.hide_network,
+ hideFollows: this.$store.state.users.currentUser.hide_follows,
+ hideFollowers: this.$store.state.users.currentUser.hide_followers,
+ showRole: this.$store.state.users.currentUser.show_role,
+ role: this.$store.state.users.currentUser.role,
followList: null,
followImportError: false,
followsImported: false,
@@ -36,7 +42,8 @@ const UserSettings = {
},
components: {
StyleSwitcher,
- TabSwitcher
+ TabSwitcher,
+ AutoCompleteInput
},
computed: {
user () {
@@ -66,7 +73,10 @@ const UserSettings = {
/* eslint-disable camelcase */
const default_scope = this.newDefaultScope
const no_rich_text = this.newNoRichText
- const hide_network = this.newHideNetwork
+ const hide_follows = this.hideFollows
+ const hide_followers = this.hideFollowers
+ const show_role = this.showRole
+
/* eslint-enable camelcase */
this.$store.state.api.backendInteractor
.updateProfile({
@@ -78,7 +88,9 @@ const UserSettings = {
/* eslint-disable camelcase */
default_scope,
no_rich_text,
- hide_network
+ hide_follows,
+ hide_followers,
+ show_role
/* eslint-enable camelcase */
}}).then((user) => {
if (!user.error) {
@@ -233,7 +245,9 @@ const UserSettings = {
exportFollows () {
this.enableFollowsExport = false
this.$store.state.api.backendInteractor
- .fetchFriends({id: this.$store.state.users.currentUser.id})
+ .exportFriends({
+ id: this.$store.state.users.currentUser.id
+ })
.then((friendList) => {
this.exportPeople(friendList, 'friends.csv')
setTimeout(() => { this.enableFollowsExport = true }, 2000)
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index be42cc4a..ad7c17bd 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -9,9 +9,9 @@
<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>
+ <auto-complete-input :classObj="{ 'name-changer': true }" :id="'username'" v-model="newName"/>
<p>{{$t('settings.bio')}}</p>
- <textarea class="bio" v-model="newBio"></textarea>
+ <auto-complete-input :classObj="{ bio: true }" v-model="newBio" :multiline="true"/>
<p>
<input type="checkbox" v-model="newLocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
@@ -30,10 +30,19 @@
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p>
<p>
- <input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
- <label for="account-hide-network">{{$t('settings.hide_network_description')}}</label>
+ <input type="checkbox" v-model="hideFollows" id="account-hide-follows">
+ <label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
</p>
- <button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
+ <p>
+ <input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
+ <label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
+ </p>
+ <p>
+ <input type="checkbox" v-model="showRole" id="account-show-role">
+ <label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
+ <label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
+ </p>
+ <button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
@@ -169,7 +178,7 @@
}
.banner {
- max-width: 400px;
+ max-width: 100%;
}
.uploading {
@@ -180,5 +189,9 @@
.name-changer {
width: 100%;
}
+
+ .bg {
+ max-width: 100%;
+ }
}
</style>