Merge branch 'emoji-selector-update' into shigusegubu
* emoji-selector-update: (48 commits) eslint fix emoji inputs in user-settings, styles update bump z-index so that picker/suggest doesn't get overlapped by mobile button Scroll emoji picker into view if it's obstructed very important fix comment, cleanup and improve autoresize/autoscroll Fix formatting in oc.json avoid using global class fix logo moving bug when lightbox is open Reserve scrollbar gap when body scroll is locked setting display: initial makes trouble, instead, toggle display: none using classname lock body scroll add body-scroll-lock directive install body-scroll-lock wire up props with PostStatusModal rename component recover autofocusing behavior refactor MobilePostStatusModal using new PostStatusModal add new module and modal to post new status remove needless condition ...
This commit is contained in:
commit
644ac2df56
46 changed files with 1072 additions and 333 deletions
|
|
@ -5,12 +5,8 @@ const conversationPage = {
|
|||
Conversation
|
||||
},
|
||||
computed: {
|
||||
statusoid () {
|
||||
const id = this.$route.params.id
|
||||
const statuses = this.$store.state.statuses.allStatusesObject
|
||||
const status = statuses[id]
|
||||
|
||||
return status
|
||||
statusId () {
|
||||
return this.$route.params.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<conversation
|
||||
:collapsable="false"
|
||||
is-page="true"
|
||||
:statusoid="statusoid"
|
||||
:status-id="statusId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { reduce, filter, findIndex, clone } from 'lodash'
|
||||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||
import Status from '../status/status.vue'
|
||||
|
||||
const sortById = (a, b) => {
|
||||
|
|
@ -39,10 +39,11 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
'statusId',
|
||||
'collapsable',
|
||||
'isPage',
|
||||
'pinnedStatusIdsObject'
|
||||
'pinnedStatusIdsObject',
|
||||
'inProfile'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
|
|
@ -51,21 +52,17 @@ const conversation = {
|
|||
},
|
||||
computed: {
|
||||
status () {
|
||||
return this.statusoid
|
||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
},
|
||||
statusId () {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return this.statusoid.retweeted_status.id
|
||||
originalStatusId () {
|
||||
if (this.status.retweeted_status) {
|
||||
return this.status.retweeted_status.id
|
||||
} else {
|
||||
return this.statusoid.id
|
||||
return this.statusId
|
||||
}
|
||||
},
|
||||
conversationId () {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return this.statusoid.retweeted_status.statusnet_conversation_id
|
||||
} else {
|
||||
return this.statusoid.statusnet_conversation_id
|
||||
}
|
||||
return this.getConversationId(this.statusId)
|
||||
},
|
||||
conversation () {
|
||||
if (!this.status) {
|
||||
|
|
@ -77,7 +74,7 @@ const conversation = {
|
|||
}
|
||||
|
||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||
const statusIndex = findIndex(conversation, { id: this.statusId })
|
||||
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
}
|
||||
|
|
@ -110,7 +107,15 @@ const conversation = {
|
|||
Status
|
||||
},
|
||||
watch: {
|
||||
status: 'fetchConversation',
|
||||
statusId (newVal, oldVal) {
|
||||
const newConversationId = this.getConversationId(newVal)
|
||||
const oldConversationId = this.getConversationId(oldVal)
|
||||
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
expanded (value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
|
|
@ -120,24 +125,25 @@ const conversation = {
|
|||
methods: {
|
||||
fetchConversation () {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.status.id })
|
||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setHighlight(this.originalStatusId)
|
||||
})
|
||||
.then(() => this.setHighlight(this.statusId))
|
||||
} else {
|
||||
const id = this.$route.params.id
|
||||
this.$store.state.api.backendInteractor.fetchStatus({ id })
|
||||
.then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
|
||||
.then(() => this.fetchConversation())
|
||||
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
})
|
||||
}
|
||||
},
|
||||
getReplies (id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
focused (id) {
|
||||
return (this.isExpanded) && id === this.status.id
|
||||
return (this.isExpanded) && id === this.statusId
|
||||
},
|
||||
setHighlight (id) {
|
||||
if (!id) return
|
||||
|
|
@ -149,6 +155,10 @@ const conversation = {
|
|||
},
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
getConversationId (statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
class="status-fadein panel-body"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Completion from '../../services/completion/completion.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
/**
|
||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||
|
|
@ -144,6 +145,7 @@ const EmojiInput = {
|
|||
input.elm.addEventListener('paste', this.onPaste)
|
||||
input.elm.addEventListener('keyup', this.onKeyUp)
|
||||
input.elm.addEventListener('keydown', this.onKeyDown)
|
||||
input.elm.addEventListener('click', this.onClickInput)
|
||||
input.elm.addEventListener('transitionend', this.onTransition)
|
||||
input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
},
|
||||
|
|
@ -155,6 +157,7 @@ const EmojiInput = {
|
|||
input.elm.removeEventListener('paste', this.onPaste)
|
||||
input.elm.removeEventListener('keyup', this.onKeyUp)
|
||||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
||||
input.elm.removeEventListener('click', this.onClickInput)
|
||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
||||
input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
}
|
||||
|
|
@ -162,6 +165,9 @@ const EmojiInput = {
|
|||
methods: {
|
||||
triggerShowPicker () {
|
||||
this.showPicker = true
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
})
|
||||
// This temporarily disables "click outside" handler
|
||||
// since external trigger also means click originates
|
||||
// from outside, thus preventing picker from opening
|
||||
|
|
@ -173,6 +179,9 @@ const EmojiInput = {
|
|||
togglePicker () {
|
||||
this.input.elm.focus()
|
||||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
this.scrollIntoView()
|
||||
}
|
||||
},
|
||||
replace (replacement) {
|
||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||
|
|
@ -267,6 +276,37 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
scrollIntoView () {
|
||||
const rootRef = this.$refs['picker'].$el
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll = scrollerRef === window
|
||||
? scrollerRef.scrollY
|
||||
: scrollerRef.scrollTop
|
||||
const scrollerHeight = scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
|
||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||
// We check where the bottom border of root element is, this uses findOffset
|
||||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
|
||||
// could also check top delta but there's no case for it
|
||||
const targetScroll = currentScroll + bottomDelta
|
||||
|
||||
if (scrollerRef === window) {
|
||||
scrollerRef.scroll(0, targetScroll)
|
||||
} else {
|
||||
scrollerRef.scrollTop = targetScroll
|
||||
}
|
||||
},
|
||||
onTransition (e) {
|
||||
this.resize()
|
||||
},
|
||||
|
|
@ -360,6 +400,9 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
onClickInput (e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
onClickOutside (e) {
|
||||
if (this.disableClickOutside) return
|
||||
this.showPicker = false
|
||||
|
|
|
|||
|
|
@ -67,9 +67,10 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0 .25em;
|
||||
margin: .2em .25em;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
||||
&:hover i {
|
||||
color: $fallback--text;
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
}
|
||||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
.autocomplete {
|
||||
&-panel {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
|
|
|
|||
|
|
@ -77,13 +77,16 @@
|
|||
</div>
|
||||
<div
|
||||
class="keep-open"
|
||||
>
|
||||
>
|
||||
<input
|
||||
:id="labelKey + 'keep-open'"
|
||||
v-model="keepOpen"
|
||||
type="checkbox"
|
||||
>
|
||||
<label class="keep-open-label" :for="labelKey + 'keep-open'">
|
||||
>
|
||||
<label
|
||||
class="keep-open-label"
|
||||
:for="labelKey + 'keep-open'"
|
||||
>
|
||||
<div class="keep-open-label-text">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@
|
|||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.muted"
|
||||
v-if="canMute && !status.thread_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"
|
||||
v-if="canMute && status.thread_muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unmuteConversation"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="showing"
|
||||
v-body-scroll-lock="showing"
|
||||
class="modal-view media-modal-view"
|
||||
@click.prevent="hide"
|
||||
>
|
||||
|
|
@ -43,6 +44,10 @@
|
|||
.media-modal-view {
|
||||
z-index: 1001;
|
||||
|
||||
body:not(.scroll-locked) & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.modal-view-button-arrow {
|
||||
opacity: 0.75;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const MobilePostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
const MobilePostStatusButton = {
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
postFormOpen: false,
|
||||
scrollingDown: false,
|
||||
inputActive: false,
|
||||
oldScrollPos: 0,
|
||||
|
|
@ -28,8 +23,8 @@ const MobilePostStatusModal = {
|
|||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
isHidden () {
|
||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||
|
|
@ -57,17 +52,7 @@ const MobilePostStatusModal = {
|
|||
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
openPostForm () {
|
||||
this.postFormOpen = true
|
||||
this.hidden = true
|
||||
|
||||
const el = this.$el.querySelector('textarea')
|
||||
this.$nextTick(function () {
|
||||
el.focus()
|
||||
})
|
||||
},
|
||||
closePostForm () {
|
||||
this.postFormOpen = false
|
||||
this.hidden = false
|
||||
this.$store.dispatch('openPostStatusModal')
|
||||
},
|
||||
handleOSK () {
|
||||
// This is a big hack: we're guessing from changed window sizes if the
|
||||
|
|
@ -105,4 +90,4 @@ const MobilePostStatusModal = {
|
|||
}
|
||||
}
|
||||
|
||||
export default MobilePostStatusModal
|
||||
export default MobilePostStatusButton
|
||||
|
|
@ -1,23 +1,5 @@
|
|||
<template>
|
||||
<div v-if="currentUser">
|
||||
<div
|
||||
v-show="postFormOpen"
|
||||
class="post-form-modal-view modal-view"
|
||||
@click="closePostForm"
|
||||
>
|
||||
<div
|
||||
class="post-form-modal-panel panel"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.new_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
@posted="closePostForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoggedIn">
|
||||
<button
|
||||
class="new-status-button"
|
||||
:class="{ 'hidden': isHidden }"
|
||||
|
|
@ -28,27 +10,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mobile_post_status_modal.js"></script>
|
||||
<script src="./mobile_post_status_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
|
|
@ -9,7 +9,8 @@ const Notification = {
|
|||
data () {
|
||||
return {
|
||||
userExpanded: false,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
|
||||
unmuted: false
|
||||
}
|
||||
},
|
||||
props: [ 'notification' ],
|
||||
|
|
@ -23,11 +24,14 @@ const Notification = {
|
|||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
generateUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
getUser (notification) {
|
||||
return this.$store.state.users.usersObject[notification.from_profile.id]
|
||||
},
|
||||
toggleMute () {
|
||||
this.unmuted = !this.unmuted
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -47,6 +51,12 @@ const Notification = {
|
|||
return this.userInStore
|
||||
}
|
||||
return this.notification.from_profile
|
||||
},
|
||||
userProfileLink () {
|
||||
return this.generateUserProfileLink(this.user)
|
||||
},
|
||||
needMute () {
|
||||
return this.user.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,104 +4,126 @@
|
|||
:compact="true"
|
||||
:statusoid="notification.status"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="non-mention"
|
||||
:class="[userClass, { highlighted: userStyle }]"
|
||||
:style="[ userStyle ]"
|
||||
>
|
||||
<a
|
||||
class="avatar-container"
|
||||
:href="notification.from_profile.statusnet_profile_url"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="needMute && !unmuted"
|
||||
class="container muted"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
v-html="notification.from_profile.name_html"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
>{{ notification.from_profile.name }}</span>
|
||||
<span v-if="notification.type === 'like'">
|
||||
<i class="fa icon-star lit" />
|
||||
<small>{{ $t('notifications.favorited_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'repeat'">
|
||||
<i
|
||||
class="fa icon-retweet lit"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
<small>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
</small>
|
||||
<a
|
||||
href="#"
|
||||
class="unmute"
|
||||
@click.prevent="toggleMute"
|
||||
><i class="button-icon icon-eye-off" /></a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="non-mention"
|
||||
:class="[userClass, { highlighted: userStyle }]"
|
||||
:style="[ userStyle ]"
|
||||
>
|
||||
<a
|
||||
class="avatar-container"
|
||||
:href="notification.from_profile.statusnet_profile_url"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
v-html="notification.from_profile.name_html"
|
||||
/>
|
||||
<small>{{ $t('notifications.repeated_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow'">
|
||||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
>{{ notification.from_profile.name }}</span>
|
||||
<span v-if="notification.type === 'like'">
|
||||
<i class="fa icon-star lit" />
|
||||
<small>{{ $t('notifications.favorited_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'repeat'">
|
||||
<i
|
||||
class="fa icon-retweet lit"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
/>
|
||||
<small>{{ $t('notifications.repeated_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow'">
|
||||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
v-if="notification.status"
|
||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
||||
class="faint-link"
|
||||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<a
|
||||
v-if="needMute"
|
||||
href="#"
|
||||
@click.prevent="toggleMute"
|
||||
><i class="button-icon icon-eye-off" /></a>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="timeago"
|
||||
class="follow-text"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
v-if="notification.status"
|
||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
||||
class="faint-link"
|
||||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
<router-link :to="userProfileLink">
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="follow-text"
|
||||
>
|
||||
<router-link :to="userProfileLink(notification.from_profile)">
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
<template v-else>
|
||||
<status
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
|
||||
.notification {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
|
@ -47,6 +46,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: .25em .6em;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j
|
|||
import { reject, map, uniqBy } from 'lodash'
|
||||
import suggestor from '../emoji_input/suggestor.js'
|
||||
|
||||
const buildMentionsString = ({ user, attentions }, currentUser) => {
|
||||
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
||||
let allAttentions = [...attentions]
|
||||
|
||||
allAttentions.unshift(user)
|
||||
|
|
@ -277,6 +277,8 @@ const PostStatusForm = {
|
|||
resize (e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
|
||||
// Reset to default height for empty form, nothing else to do here.
|
||||
if (target.value === '') {
|
||||
target.style.height = null
|
||||
this.$refs['emoji-input'].resize()
|
||||
|
|
@ -284,61 +286,74 @@ const PostStatusForm = {
|
|||
}
|
||||
|
||||
const rootRef = this.$refs['root']
|
||||
const scroller = this.$el.closest('.sidebar-scroller') ||
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
|
||||
// Getting info about padding we have to account for, removing 'px' part
|
||||
const topPaddingStr = window.getComputedStyle(target)['padding-top']
|
||||
const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
|
||||
// Remove "px" at the end of the values
|
||||
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
|
||||
const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))
|
||||
const vertPadding = topPadding + bottomPadding
|
||||
|
||||
const oldHeightStr = target.style.height || ''
|
||||
const oldHeight = Number(oldHeightStr.substring(0, oldHeightStr.length - 2))
|
||||
|
||||
const tempScroll = scroller === window ? scroller.scrollY : scroller.scrollTop
|
||||
/* Explanation:
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||
* scrollHeight returns element's scrollable content height, i.e. visible
|
||||
* element + overscrolled parts of it. We use it to determine when text
|
||||
* inside the textarea exceeded its height, so we can set height to prevent
|
||||
* overscroll, i.e. make textarea grow with the text. HOWEVER, since we
|
||||
* explicitly set new height, scrollHeight won't go below that, so we can't
|
||||
* SHRINK the textarea when there's extra space. To workaround that we set
|
||||
* height to 'auto' which makes textarea tiny again, so that scrollHeight
|
||||
* will match text height again. HOWEVER, shrinking textarea can screw with
|
||||
* the scroll since there might be not enough padding around root to even
|
||||
* warrant a scroll, so it will jump to 0 and refuse to move anywhere,
|
||||
* so we check current scroll position before shrinking and then restore it
|
||||
* with needed delta.
|
||||
*/
|
||||
|
||||
// Auto is needed to make textbox shrink when removing lines
|
||||
target.style.height = 'auto'
|
||||
const newHeight = target.scrollHeight - vertPadding
|
||||
target.style.height = `${oldHeight}px`
|
||||
|
||||
if (scroller === window) {
|
||||
scroller.scroll(0, tempScroll)
|
||||
} else {
|
||||
scroller.scrollTop = tempScroll
|
||||
}
|
||||
|
||||
const currentScroll = scroller === window ? scroller.scrollY : scroller.scrollTop
|
||||
const scrollerHeight = scroller === window ? scroller.innerHeight : scroller.offsetHeight
|
||||
// this part has to be BEFORE the content size update
|
||||
const currentScroll = scrollerRef === window
|
||||
? scrollerRef.scrollY
|
||||
: scrollerRef.scrollTop
|
||||
const scrollerHeight = scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||
|
||||
const rootBottomBorder = rootRef.offsetHeight +
|
||||
findOffset(rootRef, scroller).top
|
||||
// BEGIN content size update
|
||||
target.style.height = 'auto'
|
||||
const newHeight = target.scrollHeight - vertPadding
|
||||
target.style.height = `${newHeight}px`
|
||||
// END content size update
|
||||
|
||||
// We check where the bottom border of root element is, this uses findOffset
|
||||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const textareaSizeChangeDelta = newHeight - oldHeight || 0
|
||||
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder + textareaSizeChangeDelta
|
||||
const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
|
||||
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
|
||||
const totalDelta = textareaSizeChangeDelta +
|
||||
(isBottomObstructed ? rootChangeDelta : 0)
|
||||
|
||||
// console.log('CURRENT SCROLL', currentScroll)
|
||||
console.log('BOTTOM BORDERS', rootBottomBorder, scrollerBottomBorder)
|
||||
console.log('BOTTOM DELTA', rootBottomBorder - scrollerBottomBorder)
|
||||
const targetScroll = scrollerBottomBorder < rootBottomBorder
|
||||
? currentScroll + rootChangeDelta
|
||||
: currentScroll + textareaSizeChangeDelta
|
||||
if (scroller === window) {
|
||||
scroller.scroll(0, targetScroll)
|
||||
const targetScroll = currentScroll + totalDelta
|
||||
|
||||
if (scrollerRef === window) {
|
||||
scrollerRef.scroll(0, targetScroll)
|
||||
} else {
|
||||
scroller.scrollTop = targetScroll
|
||||
scrollerRef.scrollTop = targetScroll
|
||||
}
|
||||
target.style.height = `${newHeight}px`
|
||||
|
||||
console.log(scroller, rootRef)
|
||||
// console.log('SCROLL TO BUTTON', scrollerBottomBorder < rootBottomBorder)
|
||||
// console.log('DELTA B', rootChangeDelta)
|
||||
// console.log('DELTA D', textareaSizeChangeDelta)
|
||||
// console.log('TARGET', targetScroll)
|
||||
// console.log('ACTUAL', scroller.scrollTop || scroller.scrollY || 0)
|
||||
this.$refs['emoji-input'].resize()
|
||||
},
|
||||
showEmojiPicker () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="post-status-form"
|
||||
ref="root"
|
||||
<div
|
||||
ref="root"
|
||||
class="post-status-form"
|
||||
>
|
||||
<form
|
||||
autocomplete="off"
|
||||
|
|
|
|||
32
src/components/post_status_modal/post_status_modal.js
Normal file
32
src/components/post_status_modal/post_status_modal.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
|
||||
const PostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
isOpen () {
|
||||
return this.isLoggedIn && this.$store.state.postStatus.modalActivated
|
||||
},
|
||||
params () {
|
||||
return this.$store.state.postStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el.querySelector('textarea').focus())
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closePostStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PostStatusModal
|
||||
43
src/components/post_status_modal/post_status_modal.vue
Normal file
43
src/components/post_status_modal/post_status_modal.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="post-form-modal-view modal-view"
|
||||
@click="closeModal"
|
||||
>
|
||||
<div
|
||||
class="post-form-modal-panel panel"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.new_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./post_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,7 +29,8 @@ const Status = {
|
|||
'isPreview',
|
||||
'noHeading',
|
||||
'inlineExpanded',
|
||||
'showPinned'
|
||||
'showPinned',
|
||||
'inProfile'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
@ -117,7 +118,7 @@ const Status = {
|
|||
|
||||
return hits
|
||||
},
|
||||
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
|
||||
muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
|
||||
hideFilteredStatuses () {
|
||||
return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
|
||||
? this.$store.state.instance.hideFilteredStatuses
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ const Timeline = {
|
|||
'tag',
|
||||
'embedded',
|
||||
'count',
|
||||
'pinnedStatusIds'
|
||||
'pinnedStatusIds',
|
||||
'inProfile'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@
|
|||
v-if="timeline.statusesObject[statusId]"
|
||||
:key="statusId + '-pinned'"
|
||||
class="status-fadein"
|
||||
:statusoid="timeline.statusesObject[statusId]"
|
||||
:status-id="statusId"
|
||||
:collapsable="true"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:in-profile="inProfile"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="status in timeline.visibleStatuses">
|
||||
|
|
@ -43,8 +44,9 @@
|
|||
v-if="!excludedStatusIdsObject[status.id]"
|
||||
:key="status.id"
|
||||
class="status-fadein"
|
||||
:statusoid="status"
|
||||
:status-id="status.id"
|
||||
:collapsable="true"
|
||||
:in-profile="inProfile"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
followRequestSent: false,
|
||||
hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
|
||||
? this.$store.state.instance.hideUserStats
|
||||
: this.$store.state.config.hideUserStats,
|
||||
|
|
@ -103,9 +102,8 @@ export default {
|
|||
followUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
requestFollow(this.user, store).then(({ sent }) => {
|
||||
requestFollow(this.user, store).then(() => {
|
||||
this.followRequestInProgress = false
|
||||
this.followRequestSent = sent
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
|
|
@ -161,6 +159,9 @@ export default {
|
|||
}
|
||||
this.$store.dispatch('setMedia', [attachment])
|
||||
this.$store.dispatch('setCurrent', attachment)
|
||||
},
|
||||
mentionUser () {
|
||||
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
class="user-card"
|
||||
:class="classes"
|
||||
>
|
||||
<div :class="{ 'hide-bio': hideBio }" :style="style" class="background-image">
|
||||
</div>
|
||||
<div
|
||||
:class="{ 'hide-bio': hideBio }"
|
||||
:style="style"
|
||||
class="background-image"
|
||||
/>
|
||||
<div class="panel-heading">
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
|
|
@ -136,13 +139,13 @@
|
|||
<button
|
||||
class="btn btn-default btn-block"
|
||||
:disabled="followRequestInProgress"
|
||||
:title="followRequestSent ? $t('user_card.follow_again') : ''"
|
||||
:title="user.requested ? $t('user_card.follow_again') : ''"
|
||||
@click="followUser"
|
||||
>
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else-if="followRequestSent">
|
||||
<template v-else-if="user.requested">
|
||||
{{ $t('user_card.follow_sent') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
|
|
@ -189,6 +192,15 @@
|
|||
</ProgressButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
@click="mentionUser"
|
||||
>
|
||||
{{ $t('user_card.mention') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
rounded="top"
|
||||
/>
|
||||
<div class="panel-footer">
|
||||
<PostStatusForm v-if="user" />
|
||||
<PostStatusForm />
|
||||
</div>
|
||||
</div>
|
||||
<auth-form
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
timeline-name="user"
|
||||
:user-id="userId"
|
||||
:pinned-status-ids="user.pinnedStatusIds"
|
||||
:in-profile="true"
|
||||
/>
|
||||
<div
|
||||
v-if="followsTabVisible"
|
||||
|
|
@ -69,6 +70,7 @@
|
|||
timeline-name="media"
|
||||
:timeline="media"
|
||||
:user-id="userId"
|
||||
:in-profile="true"
|
||||
/>
|
||||
<Timeline
|
||||
v-if="isUs"
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
:title="$t('user_card.favorites')"
|
||||
timeline-name="favorites"
|
||||
:timeline="favorites"
|
||||
:in-profile="true"
|
||||
/>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<input
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
<p>{{ $t('settings.bio') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<textarea
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue