Merge remote-tracking branch 'upstream/develop' into emoji-selector-update
* upstream/develop: (116 commits) Password reset page add a comment force img updating immediately Fixed "sequimiento" to "seguimiento". Replace `/api/externalprofile/show.json` with a MastoAPI equialent Use mastodon api in follow requests "Optional" in lowercase. Update es.json fix pin/unpin status logic rename a mutation update fix user avatar fallback logic remove dead code Corrected "Media Proxy" translation. Update es.json make bio textarea resizable vertically only remove dead code Make image orientation consistent on FF, fix videos w/ modal remove dead code fix crazy watch logic in conversation ...
This commit is contained in:
commit
db086fe1fd
80 changed files with 2652 additions and 340 deletions
|
@ -190,6 +190,7 @@
|
|||
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
|
@ -286,7 +287,7 @@
|
|||
}
|
||||
|
||||
img {
|
||||
image-orientation: from-image;
|
||||
image-orientation: from-image; // NOTE: only FF supports this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
&-expanded-content {
|
||||
flex: 1;
|
||||
margin-left: 0.7em;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -42,7 +42,7 @@ const conversation = {
|
|||
'statusoid',
|
||||
'collapsable',
|
||||
'isPage',
|
||||
'showPinned'
|
||||
'pinnedStatusIdsObject'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
|
@ -110,7 +110,7 @@ const conversation = {
|
|||
Status
|
||||
},
|
||||
watch: {
|
||||
'$route': 'fetchConversation',
|
||||
status: 'fetchConversation',
|
||||
expanded (value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
|
@ -149,9 +149,6 @@ const conversation = {
|
|||
},
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
if (!this.expanded) {
|
||||
this.setHighlight(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="showPinned"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
|
|
@ -16,6 +16,16 @@ const ExtraButtons = {
|
|||
this.$store.dispatch('unpinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
muteConversation () {
|
||||
this.$store.dispatch('muteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unmuteConversation () {
|
||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -31,8 +41,8 @@ const ExtraButtons = {
|
|||
canPin () {
|
||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||
},
|
||||
enabled () {
|
||||
return this.canPin || this.canDelete
|
||||
canMute () {
|
||||
return !!this.currentUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<v-popover
|
||||
v-if="enabled"
|
||||
v-if="canDelete || canMute || canPin"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
|
@ -9,6 +9,20 @@
|
|||
>
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="muteConversation"
|
||||
>
|
||||
<i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canMute && status.muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unmuteConversation"
|
||||
>
|
||||
<i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
v-close-popover
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
const FeaturesPanel = {
|
||||
computed: {
|
||||
chat: function () {
|
||||
return this.$store.state.instance.chatAvailable && (!this.$store.state.chatDisabled)
|
||||
},
|
||||
chat: function () { return this.$store.state.instance.chatAvailable },
|
||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
|
|
|
@ -61,13 +61,17 @@
|
|||
}
|
||||
|
||||
&.contain-fit {
|
||||
img, video {
|
||||
img,
|
||||
video,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.cover-fit {
|
||||
img, video {
|
||||
img,
|
||||
video,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@ const InstanceSpecificPanel = {
|
|||
computed: {
|
||||
instanceSpecificPanelContent () {
|
||||
return this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
show () {
|
||||
return !this.$store.state.config.hideISP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="instance-specific-panel"
|
||||
>
|
||||
<div class="instance-specific-panel">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
|
@ -14,6 +11,3 @@
|
|||
</template>
|
||||
|
||||
<script src="./instance_specific_panel.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
@ -13,8 +13,8 @@ const Interactions = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onModeSwitch (index, dataset) {
|
||||
this.filterMode = tabModeDict[dataset.filter]
|
||||
onModeSwitch (key) {
|
||||
this.filterMode = tabModeDict[key]
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -10,18 +10,15 @@
|
|||
:on-switch="onModeSwitch"
|
||||
>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="mentions"
|
||||
key="mentions"
|
||||
:label="$t('nav.mentions')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="likes+repeats"
|
||||
key="likes+repeats"
|
||||
:label="$t('interactions.favs_repeats')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="follows"
|
||||
key="follows"
|
||||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
</tab-switcher>
|
||||
|
|
|
@ -5,6 +5,11 @@ const LinkPreview = {
|
|||
'size',
|
||||
'nsfw'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
imageLoaded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
useImage () {
|
||||
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
||||
|
@ -15,6 +20,15 @@ const LinkPreview = {
|
|||
useDescription () {
|
||||
return this.card.description && /\S/.test(this.card.description)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.useImage) {
|
||||
const newImg = new Image()
|
||||
newImg.onload = () => {
|
||||
this.imageLoaded = true
|
||||
}
|
||||
newImg.src = this.card.image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
rel="noopener"
|
||||
>
|
||||
<div
|
||||
v-if="useImage"
|
||||
v-if="useImage && imageLoaded"
|
||||
class="card-image"
|
||||
:class="{ 'small-image': size === 'small' }"
|
||||
>
|
||||
|
|
|
@ -33,6 +33,11 @@
|
|||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<router-link :to="{name: 'password-reset'}">
|
||||
{{ $t('password_reset.forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
||||
image-orientation: from-image; // NOTE: only FF supports this
|
||||
}
|
||||
|
||||
.modal-view-button-arrow {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import MobilePostStatusModal from '../mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
|
||||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications,
|
||||
MobilePostStatusModal
|
||||
Notifications
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
|
|
|
@ -70,7 +70,6 @@
|
|||
ref="sideDrawer"
|
||||
:logout="logout"
|
||||
/>
|
||||
<MobilePostStatusModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -34,14 +34,19 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin: 25% 0 4em 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
|
|
62
src/components/password_reset/password_reset.js
Normal file
62
src/components/password_reset/password_reset.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { mapState } from 'vuex'
|
||||
import passwordResetApi from '../../services/new_api/password_reset.js'
|
||||
|
||||
const passwordReset = {
|
||||
data: () => ({
|
||||
user: {
|
||||
email: ''
|
||||
},
|
||||
isPending: false,
|
||||
success: false,
|
||||
throttled: false,
|
||||
error: null
|
||||
}),
|
||||
computed: {
|
||||
...mapState({
|
||||
signedIn: (state) => !!state.users.currentUser,
|
||||
instance: state => state.instance
|
||||
}),
|
||||
mailerEnabled () {
|
||||
return this.instance.mailerEnabled
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.signedIn) {
|
||||
this.$router.push({ name: 'root' })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dismissError () {
|
||||
this.error = null
|
||||
},
|
||||
submit () {
|
||||
this.isPending = true
|
||||
const email = this.user.email
|
||||
const instance = this.instance.server
|
||||
|
||||
passwordResetApi({ instance, email }).then(({ status }) => {
|
||||
this.isPending = false
|
||||
this.user.email = ''
|
||||
|
||||
if (status === 204) {
|
||||
this.success = true
|
||||
this.error = null
|
||||
} else if (status === 404 || status === 400) {
|
||||
this.error = this.$t('password_reset.not_found')
|
||||
this.$nextTick(() => {
|
||||
this.$refs.email.focus()
|
||||
})
|
||||
} else if (status === 429) {
|
||||
this.throttled = true
|
||||
this.error = this.$t('password_reset.too_many_requests')
|
||||
}
|
||||
}).catch(() => {
|
||||
this.isPending = false
|
||||
this.user.email = ''
|
||||
this.error = this.$t('general.generic_error')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default passwordReset
|
116
src/components/password_reset/password_reset.vue
Normal file
116
src/components/password_reset/password_reset.vue
Normal file
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div class="settings panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ $t('password_reset.password_reset') }}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form
|
||||
class="password-reset-form"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<div class="container">
|
||||
<div v-if="!mailerEnabled">
|
||||
<p>
|
||||
{{ $t('password_reset.password_reset_disabled') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="success || throttled">
|
||||
<p v-if="success">
|
||||
{{ $t('password_reset.check_email') }}
|
||||
</p>
|
||||
<div class="form-group text-center">
|
||||
<router-link :to="{name: 'root'}">
|
||||
{{ $t('password_reset.return_home') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>
|
||||
{{ $t('password_reset.instruction') }}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input
|
||||
ref="email"
|
||||
v-model="user.email"
|
||||
:disabled="isPending"
|
||||
:placeholder="$t('password_reset.placeholder')"
|
||||
class="form-control"
|
||||
type="input"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
:disabled="isPending"
|
||||
type="submit"
|
||||
class="btn btn-default btn-block"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="error"
|
||||
class="alert error notice-dismissible"
|
||||
>
|
||||
<span>{{ error }}</span>
|
||||
<a
|
||||
class="button-icon dismiss"
|
||||
@click.prevent="dismissError()"
|
||||
>
|
||||
<i class="icon-cancel" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./password_reset.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.password-reset-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0.6em;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex-direction: column;
|
||||
margin-top: 0.6em;
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
padding: 0.3em 0.0em 0.3em;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
animation-name: shakeError;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.5em;
|
||||
margin: 0.3em 0.0em 1em;
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.icon-cancel {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -268,6 +268,7 @@ $validations-cRed: #f04124;
|
|||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
|
|
|
@ -75,8 +75,8 @@ const Search = {
|
|||
const length = this[tabName].length
|
||||
return length === 0 ? '' : ` (${length})`
|
||||
},
|
||||
onResultTabSwitch (_index, dataset) {
|
||||
this.currenResultTab = dataset.filter
|
||||
onResultTabSwitch (key) {
|
||||
this.currenResultTab = key
|
||||
},
|
||||
getActiveTab () {
|
||||
if (this.visibleStatuses.length > 0) {
|
||||
|
|
|
@ -31,21 +31,18 @@
|
|||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
:on-switch="onResultTabSwitch"
|
||||
:custom-active="currenResultTab"
|
||||
:active-tab="currenResultTab"
|
||||
>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="statuses"
|
||||
key="statuses"
|
||||
:label="$t('user_card.statuses') + resultCount('visibleStatuses')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="people"
|
||||
key="people"
|
||||
:label="$t('search.people') + resultCount('users')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="hashtags"
|
||||
key="hashtags"
|
||||
:label="$t('search.hashtags') + resultCount('hashtags')"
|
||||
/>
|
||||
</tab-switcher>
|
||||
|
|
|
@ -20,6 +20,11 @@ const SearchBar = {
|
|||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
this.$emit('toggled', this.hidden)
|
||||
this.$nextTick(() => {
|
||||
if (!this.hidden) {
|
||||
this.$refs.searchInput.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -335,7 +335,7 @@ const Status = {
|
|||
return
|
||||
}
|
||||
}
|
||||
if (target.className.match(/hashtag/)) {
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from link url
|
||||
const tag = extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="showPinned && statusoid.pinned"
|
||||
v-if="showPinned"
|
||||
class="status-pin"
|
||||
>
|
||||
<i class="fa icon-pin faint" />
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
v-if="animated"
|
||||
ref="canvas"
|
||||
/>
|
||||
<!-- NOTE: key is required to force to re-render img tag when src is changed -->
|
||||
<img
|
||||
ref="src"
|
||||
:key="src"
|
||||
:src="src"
|
||||
:referrerpolicy="referrerpolicy"
|
||||
@load="onLoad"
|
||||
|
|
|
@ -14,7 +14,7 @@ export default Vue.component('tab-switcher', {
|
|||
required: false,
|
||||
type: Function
|
||||
},
|
||||
customActive: {
|
||||
activeTab: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
|
@ -29,6 +29,16 @@ export default Vue.component('tab-switcher', {
|
|||
active: this.$slots.default.findIndex(_ => _.tag)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeIndex () {
|
||||
// In case of controlled component
|
||||
if (this.activeTab) {
|
||||
return this.$slots.default.findIndex(slot => this.activeTab === slot.key)
|
||||
} else {
|
||||
return this.active
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
const currentSlot = this.$slots.default[this.active]
|
||||
if (!currentSlot.tag) {
|
||||
|
@ -36,22 +46,14 @@ export default Vue.component('tab-switcher', {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
activateTab (index, dataset) {
|
||||
activateTab (index) {
|
||||
return (e) => {
|
||||
e.preventDefault()
|
||||
if (typeof this.onSwitch === 'function') {
|
||||
this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
|
||||
this.onSwitch.call(null, this.$slots.default[index].key)
|
||||
}
|
||||
this.active = index
|
||||
}
|
||||
},
|
||||
isActiveTab (index) {
|
||||
const customActiveIndex = this.$slots.default.findIndex(slot => {
|
||||
const dataFilter = slot.data && slot.data.attrs && slot.data.attrs['data-filter']
|
||||
return this.customActive && this.customActive === dataFilter
|
||||
})
|
||||
|
||||
return customActiveIndex > -1 ? customActiveIndex === index : index === this.active
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
|
@ -61,13 +63,13 @@ export default Vue.component('tab-switcher', {
|
|||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
if (this.isActiveTab(index)) {
|
||||
if (this.activeIndex === index) {
|
||||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
}
|
||||
if (slot.data.attrs.image) {
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<div class={classesWrapper.join(' ')}>
|
||||
<button
|
||||
disabled={slot.data.attrs.disabled}
|
||||
onClick={this.activateTab(index)}
|
||||
|
@ -79,7 +81,7 @@ export default Vue.component('tab-switcher', {
|
|||
)
|
||||
}
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<div class={classesWrapper.join(' ')}>
|
||||
<button
|
||||
disabled={slot.data.attrs.disabled}
|
||||
onClick={this.activateTab(index)}
|
||||
|
@ -91,7 +93,7 @@ export default Vue.component('tab-switcher', {
|
|||
|
||||
const contents = this.$slots.default.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const active = index === this.active
|
||||
const active = this.activeIndex === index
|
||||
if (this.renderOnlyFocused) {
|
||||
return active
|
||||
? <div class="active">{slot}</div>
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
import Status from '../status/status.vue'
|
||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { throttle, keyBy } from 'lodash'
|
||||
|
||||
export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
|
||||
const ids = []
|
||||
if (pinnedStatusIds && pinnedStatusIds.length > 0) {
|
||||
for (let status of statuses) {
|
||||
if (!pinnedStatusIds.includes(status.id)) {
|
||||
break
|
||||
}
|
||||
ids.push(status.id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
const Timeline = {
|
||||
props: [
|
||||
|
@ -11,7 +24,8 @@ const Timeline = {
|
|||
'userId',
|
||||
'tag',
|
||||
'embedded',
|
||||
'count'
|
||||
'count',
|
||||
'pinnedStatusIds'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -39,6 +53,15 @@ const Timeline = {
|
|||
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
||||
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
||||
}
|
||||
},
|
||||
// id map of statuses which need to be hidden in the main list due to pinning logic
|
||||
excludedStatusIdsObject () {
|
||||
const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds)
|
||||
// Convert id array to object
|
||||
return keyBy(ids)
|
||||
},
|
||||
pinnedStatusIdsObject () {
|
||||
return keyBy(this.pinnedStatusIds)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -28,13 +28,25 @@
|
|||
</div>
|
||||
<div :class="classes.body">
|
||||
<div class="timeline">
|
||||
<conversation
|
||||
v-for="status in timeline.visibleStatuses"
|
||||
:key="status.id"
|
||||
class="status-fadein"
|
||||
:statusoid="status"
|
||||
:collapsable="true"
|
||||
/>
|
||||
<template v-for="statusId in pinnedStatusIds">
|
||||
<conversation
|
||||
v-if="timeline.statusesObject[statusId]"
|
||||
:key="statusId + '-pinned'"
|
||||
class="status-fadein"
|
||||
:statusoid="timeline.statusesObject[statusId]"
|
||||
:collapsable="true"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="status in timeline.visibleStatuses">
|
||||
<conversation
|
||||
v-if="!excludedStatusIdsObject[status.id]"
|
||||
:key="status.id"
|
||||
class="status-fadein"
|
||||
:statusoid="status"
|
||||
:collapsable="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="classes.footer">
|
||||
|
|
|
@ -16,7 +16,7 @@ const UserAvatar = {
|
|||
},
|
||||
computed: {
|
||||
imgSrc () {
|
||||
return this.showPlaceholder ? '/images/avi.png' : this.src
|
||||
return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class="avatar"
|
||||
:alt="user.screen_name"
|
||||
:title="user.screen_name"
|
||||
:src="user.profile_image_url_original"
|
||||
:src="imgSrc"
|
||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||
:image-load-error="imageLoadError"
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
export default {
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ],
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ],
|
||||
data () {
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
|
@ -162,6 +162,14 @@ export default {
|
|||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', this.user.id)
|
||||
},
|
||||
zoomAvatar () {
|
||||
const attachment = {
|
||||
url: this.user.profile_image_url_original,
|
||||
mimetype: 'image'
|
||||
}
|
||||
this.$store.dispatch('setMedia', [attachment])
|
||||
this.$store.dispatch('setCurrent', attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,23 @@
|
|||
<div class="panel-heading">
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<a
|
||||
v-if="allowZoomingAvatar"
|
||||
class="user-info-avatar-link"
|
||||
@click="zoomAvatar"
|
||||
>
|
||||
<UserAvatar
|
||||
:better-shadow="betterShadow"
|
||||
:user="user"
|
||||
/>
|
||||
<div class="user-info-avatar-link-overlay">
|
||||
<i class="button-icon icon-zoom-in" />
|
||||
</div>
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
<UserAvatar
|
||||
:better-shadow="betterShadow"
|
||||
:user="user"
|
||||
|
@ -351,6 +367,7 @@
|
|||
.container {
|
||||
padding: 16px 0 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
max-height: 56px;
|
||||
|
||||
.avatar {
|
||||
|
@ -372,6 +389,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-avatar-link {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease;
|
||||
|
||||
i {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.usersettings {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
|
|
@ -22,21 +22,23 @@ const FriendList = withLoadMore({
|
|||
additionalPropNames: ['userId']
|
||||
})(List)
|
||||
|
||||
const defaultTabKey = 'statuses'
|
||||
|
||||
const UserProfile = {
|
||||
data () {
|
||||
return {
|
||||
error: false,
|
||||
userId: null
|
||||
userId: null,
|
||||
tab: defaultTabKey
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// Make sure that timelines used in this page are empty
|
||||
this.cleanUp()
|
||||
const routeParams = this.$route.params
|
||||
this.load(routeParams.name || routeParams.id)
|
||||
this.tab = get(this.$route, 'query.tab', defaultTabKey)
|
||||
},
|
||||
destroyed () {
|
||||
this.cleanUp()
|
||||
this.stopFetching()
|
||||
},
|
||||
computed: {
|
||||
timeline () {
|
||||
|
@ -67,17 +69,36 @@ const UserProfile = {
|
|||
},
|
||||
methods: {
|
||||
load (userNameOrId) {
|
||||
const startFetchingTimeline = (timeline, userId) => {
|
||||
// Clear timeline only if load another user's profile
|
||||
if (userId !== this.$store.state.statuses.timelines[timeline].userId) {
|
||||
this.$store.commit('clearTimeline', { timeline })
|
||||
}
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline, userId })
|
||||
}
|
||||
|
||||
const loadById = (userId) => {
|
||||
this.userId = userId
|
||||
startFetchingTimeline('user', userId)
|
||||
startFetchingTimeline('media', userId)
|
||||
if (this.isUs) {
|
||||
startFetchingTimeline('favorites', userId)
|
||||
}
|
||||
// Fetch all pinned statuses immediately
|
||||
this.$store.dispatch('fetchPinnedStatuses', userId)
|
||||
}
|
||||
|
||||
// Reset view
|
||||
this.userId = null
|
||||
this.error = false
|
||||
|
||||
// Check if user data is already loaded in store
|
||||
const user = this.$store.getters.findUser(userNameOrId)
|
||||
if (user) {
|
||||
this.userId = user.id
|
||||
this.fetchTimelines()
|
||||
loadById(user.id)
|
||||
} else {
|
||||
this.$store.dispatch('fetchUser', userNameOrId)
|
||||
.then(({ id }) => {
|
||||
this.userId = id
|
||||
this.fetchTimelines()
|
||||
})
|
||||
.then(({ id }) => loadById(id))
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
|
@ -90,40 +111,33 @@ const UserProfile = {
|
|||
})
|
||||
}
|
||||
},
|
||||
fetchTimelines () {
|
||||
const userId = this.userId
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
|
||||
}
|
||||
// Fetch all pinned statuses immediately
|
||||
this.$store.dispatch('fetchPinnedStatuses', userId)
|
||||
},
|
||||
cleanUp () {
|
||||
stopFetching () {
|
||||
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' })
|
||||
},
|
||||
switchUser (userNameOrId) {
|
||||
this.stopFetching()
|
||||
this.load(userNameOrId)
|
||||
},
|
||||
onTabSwitch (tab) {
|
||||
this.tab = tab
|
||||
this.$router.replace({ query: { tab } })
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.load(newVal)
|
||||
this.switchUser(newVal)
|
||||
}
|
||||
},
|
||||
'$route.params.name': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.load(newVal)
|
||||
this.switchUser(newVal)
|
||||
}
|
||||
},
|
||||
$route () {
|
||||
this.$refs.tabSwitcher.activateTab(0)()
|
||||
'$route.query': function (newVal) {
|
||||
this.tab = newVal.tab || defaultTabKey
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -8,36 +8,28 @@
|
|||
:user="user"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
:allow-zooming-avatar="true"
|
||||
rounded="top"
|
||||
/>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
:active-tab="tab"
|
||||
:render-only-focused="true"
|
||||
:on-switch="onTabSwitch"
|
||||
>
|
||||
<div :label="$t('user_card.statuses')">
|
||||
<div class="timeline">
|
||||
<template v-for="statusId in user.pinnedStatuseIds">
|
||||
<Conversation
|
||||
v-if="timeline.statusesObject[statusId]"
|
||||
:key="statusId"
|
||||
class="status-fadein"
|
||||
:statusoid="timeline.statusesObject[statusId]"
|
||||
:collapsable="true"
|
||||
:show-pinned="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<Timeline
|
||||
:count="user.statuses_count"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
:user-id="userId"
|
||||
/>
|
||||
</div>
|
||||
<Timeline
|
||||
key="statuses"
|
||||
:label="$t('user_card.statuses')"
|
||||
:count="user.statuses_count"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
timeline-name="user"
|
||||
:user-id="userId"
|
||||
:pinned-status-ids="user.pinnedStatusIds"
|
||||
/>
|
||||
<div
|
||||
v-if="followsTabVisible"
|
||||
key="followees"
|
||||
:label="$t('user_card.followees')"
|
||||
:disabled="!user.friends_count"
|
||||
>
|
||||
|
@ -52,6 +44,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="followersTabVisible"
|
||||
key="followers"
|
||||
:label="$t('user_card.followers')"
|
||||
:disabled="!user.followers_count"
|
||||
>
|
||||
|
@ -68,6 +61,7 @@
|
|||
</FollowerList>
|
||||
</div>
|
||||
<Timeline
|
||||
key="media"
|
||||
:label="$t('user_card.media')"
|
||||
:disabled="!media.visibleStatuses.length"
|
||||
:embedded="true"
|
||||
|
@ -78,6 +72,7 @@
|
|||
/>
|
||||
<Timeline
|
||||
v-if="isUs"
|
||||
key="favorites"
|
||||
:label="$t('user_card.favorites')"
|
||||
:disabled="!favorites.visibleStatuses.length"
|
||||
:embedded="true"
|
||||
|
|
|
@ -21,11 +21,12 @@ const WhoToFollow = {
|
|||
name: i.display_name,
|
||||
screen_name: i.acct,
|
||||
profile_image_url: i.avatar || '/images/avi.png',
|
||||
profile_image_url_original: i.avatar || '/images/avi.png'
|
||||
profile_image_url_original: i.avatar || '/images/avi.png',
|
||||
statusnet_profile_url: i.url
|
||||
}
|
||||
this.users.push(user)
|
||||
|
||||
this.$store.state.api.backendInteractor.externalProfile(user.screen_name)
|
||||
this.$store.state.api.backendInteractor.fetchUser({ id: user.screen_name })
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
this.$store.commit('addNewUsers', [externalUser])
|
||||
|
|
|
@ -13,7 +13,7 @@ function showWhoToFollow (panel, reply) {
|
|||
toFollow.img = img
|
||||
toFollow.name = name
|
||||
|
||||
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||
panel.$store.state.api.backendInteractor.fetchUser({ id: name })
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
panel.$store.commit('addNewUsers', [externalUser])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue