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:
Henry Jameson 2019-09-25 20:27:46 +03:00
commit 644ac2df56
46 changed files with 1072 additions and 333 deletions

View file

@ -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
}
}
}

View file

@ -2,7 +2,7 @@
<conversation
:collapsable="false"
is-page="true"
:statusoid="statusoid"
:status-id="statusId"
/>
</template>

View file

@ -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'))
}
}
}

View file

@ -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"

View file

@ -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

View file

@ -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 {

View file

@ -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>

View file

@ -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"
>

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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
}
}
}

View file

@ -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>

View file

@ -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;

View file

@ -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 () {

View file

@ -1,7 +1,7 @@
<template>
<div
class="post-status-form"
ref="root"
<div
ref="root"
class="post-status-form"
>
<form
autocomplete="off"

View 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

View 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>

View file

@ -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

View file

@ -25,7 +25,8 @@ const Timeline = {
'tag',
'embedded',
'count',
'pinnedStatusIds'
'pinnedStatusIds',
'inProfile'
],
data () {
return {

View file

@ -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>

View file

@ -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 })
}
}
}

View file

@ -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"

View file

@ -11,7 +11,7 @@
rounded="top"
/>
<div class="panel-footer">
<PostStatusForm v-if="user" />
<PostStatusForm />
</div>
</div>
<auth-form

View file

@ -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>

View file

@ -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