make replies inline
This commit is contained in:
parent
3b8f551477
commit
5a0c4897b0
|
@ -1,4 +1,4 @@
|
|||
<div class="{{className}} {{hideAndFadeIn}}">
|
||||
<div class="{{computedClassName}} {{hideAndFadeIn}}">
|
||||
<ComposeAuthor />
|
||||
{{#if contentWarningShown}}
|
||||
<div class="compose-content-warning-wrapper"
|
||||
|
@ -13,10 +13,12 @@
|
|||
<ComposeMedia :realm :media />
|
||||
</div>
|
||||
<div class="compose-box-button-sentinel {{hideAndFadeIn}}" ref:sentinel></div>
|
||||
<div class="compose-box-button-wrapper {{hideAndFadeIn}}" >
|
||||
<div class="compose-box-button-wrapper {{realm === 'home' ? 'compose-button-sticky' : ''}} {{hideAndFadeIn}}" >
|
||||
<ComposeButton :length :overLimit :sticky on:click="onClickPostButton()" />
|
||||
</div>
|
||||
<div class="compose-box-border-bottom {{hideAndFadeIn}}"></div>
|
||||
{{#if !hideBottomBorder}}
|
||||
<div class="compose-box-border-bottom {{hideAndFadeIn}}"></div>
|
||||
{{/if}}
|
||||
<style>
|
||||
.compose-box {
|
||||
border-radius: 4px;
|
||||
|
@ -35,7 +37,7 @@
|
|||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
|
||||
.compose-box.dialog-size {
|
||||
.compose-box.slim-size {
|
||||
width: 540px;
|
||||
max-width: calc(100vw - 60px);
|
||||
}
|
||||
|
@ -54,11 +56,14 @@
|
|||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.compose-box-button-wrapper.compose-button-sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.compose-content-warning-wrapper {
|
||||
|
@ -71,7 +76,7 @@
|
|||
max-width: calc(100vw - 20px);
|
||||
width: 580px;
|
||||
}
|
||||
.compose-box.dialog-size {
|
||||
.compose-box.slim-size {
|
||||
width: 560px;
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
|
@ -115,10 +120,6 @@
|
|||
return
|
||||
}
|
||||
this.fire('postedStatus')
|
||||
// if this is a reply, go back immediately after it's posted
|
||||
if (realm !== 'home' && realm !== 'dialog') {
|
||||
window.history.back()
|
||||
}
|
||||
}, {init: false})
|
||||
},
|
||||
ondestroy() {
|
||||
|
@ -138,11 +139,11 @@
|
|||
},
|
||||
store: () => store,
|
||||
computed: {
|
||||
className: (overLimit, realm) => {
|
||||
computedClassName: (overLimit, realm, size) => {
|
||||
return classname(
|
||||
'compose-box',
|
||||
overLimit && 'over-char-limit',
|
||||
realm === 'dialog' && 'dialog-size'
|
||||
size === 'slim' && 'slim-size'
|
||||
)
|
||||
},
|
||||
hideAndFadeIn: (hidden) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ModalDialog :label :shown :closed :title background="var(--main-bg)">
|
||||
<ComposeBox realm="dialog" on:postedStatus="onPostedStatus()" />
|
||||
<ComposeBox realm="dialog" size="slim" on:postedStatus="onPostedStatus()" />
|
||||
</ModalDialog>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
|
|
|
@ -35,7 +35,11 @@
|
|||
<StatusDetails :originalStatus :originalStatusId />
|
||||
{{/if}}
|
||||
<StatusToolbar :originalStatus :originalStatusId :originalAccountId
|
||||
:isStatusInOwnThread :uuid :visibility />
|
||||
:isStatusInOwnThread :uuid :visibility :replyShown
|
||||
on:recalculateHeight />
|
||||
{{#if replyShown}}
|
||||
<StatusComposeBox :originalStatusId :uuid on:recalculateHeight />
|
||||
{{/if}}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
|
@ -51,9 +55,10 @@
|
|||
"sidebar spoiler-btn spoiler-btn spoiler-btn"
|
||||
"sidebar content content content"
|
||||
"media media media media"
|
||||
"....... toolbar toolbar toolbar";
|
||||
"....... toolbar toolbar toolbar"
|
||||
"compose compose compose compose";
|
||||
grid-template-columns: min-content minmax(0, max-content) 1fr min-content;
|
||||
grid-template-rows: repeat(7, max-content);
|
||||
grid-template-rows: repeat(8, max-content);
|
||||
}
|
||||
|
||||
.status-article.status-in-timeline {
|
||||
|
@ -74,9 +79,10 @@
|
|||
"content content"
|
||||
"media media"
|
||||
"details details"
|
||||
"toolbar toolbar";
|
||||
"toolbar toolbar"
|
||||
"compose compose";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: repeat(6, max-content);
|
||||
grid-template-rows: repeat(7, max-content);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
@ -100,11 +106,14 @@
|
|||
import StatusMediaAttachments from './StatusMediaAttachments.html'
|
||||
import StatusContent from './StatusContent.html'
|
||||
import StatusSpoiler from './StatusSpoiler.html'
|
||||
import StatusComposeBox from './StatusComposeBox.html'
|
||||
import { store } from '../../_store/store'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { registerClickDelegate, unregisterClickDelegate } from '../../_utils/delegate'
|
||||
import { classname } from '../../_utils/classname'
|
||||
|
||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
let delegateKey = this.get('delegateKey')
|
||||
|
@ -129,7 +138,8 @@
|
|||
StatusToolbar,
|
||||
StatusMediaAttachments,
|
||||
StatusContent,
|
||||
StatusSpoiler
|
||||
StatusSpoiler,
|
||||
StatusComposeBox
|
||||
},
|
||||
store: () => store,
|
||||
methods: {
|
||||
|
@ -138,12 +148,9 @@
|
|||
let { localName, parentElement } = e.target
|
||||
|
||||
if ((type === 'click' || (type === 'keydown' && keyCode === 13)) &&
|
||||
localName !== 'a' &&
|
||||
localName !== 'button' &&
|
||||
parentElement.localName !== 'a' &&
|
||||
parentElement.localName !== 'button' &&
|
||||
parentElement.parentElement.localName !== 'a' &&
|
||||
parentElement.parentElement.localName !== 'button') {
|
||||
!INPUT_TAGS.has(localName) &&
|
||||
!INPUT_TAGS.has(parentElement.localName) &&
|
||||
!INPUT_TAGS.has(parentElement.parentElement.localName)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
goto(`/statuses/${this.get('originalStatusId')}`)
|
||||
|
@ -174,6 +181,7 @@
|
|||
notification.type !== 'mention' && notification.status.id === originalStatusId
|
||||
},
|
||||
spoilerShown: ($spoilersShown, uuid) => !!$spoilersShown[uuid],
|
||||
replyShown: ($repliesShown, uuid) => !!$repliesShown[uuid],
|
||||
ariaLabel: (originalAccount, originalStatus, visibility) => {
|
||||
return (visibility === 'direct' ? 'Direct message' : 'Status') +
|
||||
` by ${originalAccount.display_name || originalAccount.username}`
|
||||
|
|
57
routes/_components/status/StatusComposeBox.html
Normal file
57
routes/_components/status/StatusComposeBox.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<div class="status-article-compose-box">
|
||||
<ComposeBox realm="{{originalStatusId}}"
|
||||
size="slim"
|
||||
autoFocus="true"
|
||||
hideBottomBorder="true"
|
||||
on:postedStatus="onPostedStatus()"
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
.status-article-compose-box {
|
||||
grid-area: compose;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ComposeBox from '../../_components/compose/ComposeBox.html'
|
||||
import { store } from '../../_store/store'
|
||||
import { doubleRAF } from '../../_utils/doubleRAF'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
let lastContentWarningShown = false
|
||||
this.observe('composeData', composeData => {
|
||||
doubleRAF(() => {
|
||||
this.fire('recalculateHeight')
|
||||
let contentWarningShown = !!composeData.contentWarningShown
|
||||
if (contentWarningShown !== lastContentWarningShown) {
|
||||
// TODO: this animation lasts 333ms, hence need to recalculate again
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.fire('recalculateHeight')
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
lastContentWarningShown = contentWarningShown
|
||||
})
|
||||
}, {init: false})
|
||||
},
|
||||
components: {
|
||||
ComposeBox
|
||||
},
|
||||
store: () => store,
|
||||
computed: {
|
||||
composeData: ($currentComposeData, originalStatusId) => $currentComposeData[originalStatusId] || {},
|
||||
},
|
||||
methods: {
|
||||
onPostedStatus() {
|
||||
requestAnimationFrame(() => {
|
||||
let uuid = this.get('uuid')
|
||||
let $repliesShown = this.store.get('repliesShown')
|
||||
$repliesShown[uuid] = false
|
||||
this.store.set({'repliesShown': $repliesShown})
|
||||
this.fire('recalculateHeight')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,9 @@
|
|||
<div class="status-toolbar {{isStatusInOwnThread ? 'status-in-own-thread' : ''}}">
|
||||
<IconButton
|
||||
label="Reply"
|
||||
label="{{replyLabel}}"
|
||||
pressable="true"
|
||||
pressed="{{replyShown}}"
|
||||
href="#fa-reply"
|
||||
disabled="{{disableReply}}"
|
||||
delegateKey="{{replyKey}}"
|
||||
focusKey="{{replyKey}}"
|
||||
/>
|
||||
|
@ -45,17 +46,16 @@
|
|||
import { registerClickDelegate, unregisterClickDelegate } from '../../_utils/delegate'
|
||||
import { setFavorited } from '../../_actions/favorite'
|
||||
import { setReblogged } from '../../_actions/reblog'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { importDialogs } from '../../_utils/asyncModules'
|
||||
import { updateProfileAndRelationship } from '../../_actions/accounts'
|
||||
import { FAVORITE_ANIMATION, REBLOG_ANIMATION } from '../../_static/animations'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
registerClickDelegate(this.get('favoriteKey'), () => this.onFavoriteClick())
|
||||
registerClickDelegate(this.get('reblogKey'), () => this.onReblogClick())
|
||||
registerClickDelegate(this.get('replyKey'), () => this.onReplyClick())
|
||||
registerClickDelegate(this.get('optionsKey'), () => this.onOptionsClick())
|
||||
registerClickDelegate(this.get('favoriteKey'), (e) => this.onFavoriteClick(e))
|
||||
registerClickDelegate(this.get('reblogKey'), (e) => this.onReblogClick(e))
|
||||
registerClickDelegate(this.get('replyKey'), (e) => this.onReplyClick(e))
|
||||
registerClickDelegate(this.get('optionsKey'), (e) => this.onOptionsClick(e))
|
||||
},
|
||||
ondestroy() {
|
||||
unregisterClickDelegate(this.get('favoriteKey'))
|
||||
|
@ -68,23 +68,36 @@
|
|||
},
|
||||
store: () => store,
|
||||
methods: {
|
||||
onFavoriteClick() {
|
||||
onFavoriteClick(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let originalStatusId = this.get('originalStatusId')
|
||||
let favorited = this.get('favorited')
|
||||
/* no await */ setFavorited(originalStatusId, !favorited)
|
||||
this.set({animateFavorite: !favorited})
|
||||
},
|
||||
onReblogClick() {
|
||||
onReblogClick(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let originalStatusId = this.get('originalStatusId')
|
||||
let reblogged = this.get('reblogged')
|
||||
/* no await */ setReblogged(originalStatusId, !reblogged)
|
||||
this.set({animateReblog: !reblogged})
|
||||
},
|
||||
onReplyClick() {
|
||||
let originalStatusId = this.get('originalStatusId')
|
||||
goto(`/statuses/${originalStatusId}/reply`)
|
||||
onReplyClick(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
requestAnimationFrame(() => {
|
||||
let uuid = this.get('uuid')
|
||||
let $repliesShown = this.store.get('repliesShown')
|
||||
$repliesShown[uuid] = !$repliesShown[uuid]
|
||||
this.store.set({'repliesShown': $repliesShown})
|
||||
this.fire('recalculateHeight')
|
||||
})
|
||||
},
|
||||
async onOptionsClick() {
|
||||
async onOptionsClick(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let originalStatusId = this.get('originalStatusId')
|
||||
let originalAccountId = this.get('originalAccountId')
|
||||
let updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
|
||||
|
@ -98,6 +111,7 @@
|
|||
reblogAnimation: REBLOG_ANIMATION
|
||||
}),
|
||||
computed: {
|
||||
replyLabel: (replyShown) => replyShown ? 'Close reply' : 'Reply',
|
||||
reblogLabel: (visibility) => {
|
||||
switch (visibility) {
|
||||
case 'private':
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<DynamicPageBanner title=""/>
|
||||
<div class="reply-container">
|
||||
{{#if status}}
|
||||
<Status index="0"
|
||||
length="1"
|
||||
timelineType="reply"
|
||||
timelineValue="{{params.statusId}}"
|
||||
:status
|
||||
/>
|
||||
<ComposeBox realm="{{params.statusId}}" autoFocus="true" />
|
||||
{{else}}
|
||||
<LoadingPage />
|
||||
{{/if}}
|
||||
</div>
|
||||
<style>
|
||||
.reply-container {
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { store } from '../../../_store/store.js'
|
||||
import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'
|
||||
import LoadingPage from '../../../_components/LoadingPage.html'
|
||||
import ComposeBox from '../../../_components/compose/ComposeBox.html'
|
||||
import Status from '../../../_components/status/Status.html'
|
||||
import { database } from '../../../_database/database'
|
||||
|
||||
export default {
|
||||
async oncreate() {
|
||||
let statusId = this.get('params').statusId
|
||||
let instanceName = this.store.get('currentInstance')
|
||||
let status = await database.getStatus(instanceName, statusId)
|
||||
this.set({status})
|
||||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
DynamicPageBanner,
|
||||
LoadingPage,
|
||||
ComposeBox,
|
||||
Status
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -33,6 +33,7 @@ export const store = new PinaforeStore({
|
|||
instanceThemes: {},
|
||||
spoilersShown: {},
|
||||
sensitivesShown: {},
|
||||
repliesShown: {},
|
||||
autoplayGifs: false,
|
||||
markMediaAsSensitive: false,
|
||||
reduceMotion: false,
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<:Head>
|
||||
<title>Pinafore – Reply</title>
|
||||
</:Head>
|
||||
<Layout page='reply' >
|
||||
<LazyPage :pageComponent :params />
|
||||
</Layout>
|
||||
<script>
|
||||
import Layout from '../../_components/Layout.html'
|
||||
import LazyPage from '../../_components/LazyPage.html'
|
||||
import pageComponent from '../../_pages/statuses/[statusId]/reply.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
LazyPage
|
||||
},
|
||||
data: () => ({
|
||||
pageComponent
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
getNthStatus, scrollToStatus, closeDialogButton, modalDialogContents, getActiveElementClass, goBack, getUrl,
|
||||
goBackButton, getActiveElementInnerText, getNthReplyButton, getActiveElementAriaLabel, getActiveElementInsideNthStatus
|
||||
goBackButton, getActiveElementInnerText, getNthReplyButton, getActiveElementInsideNthStatus
|
||||
} from '../utils'
|
||||
import { foobarRole } from '../roles'
|
||||
|
||||
|
@ -81,12 +81,5 @@ test('thread preserves focus', async t => {
|
|||
test('reply preserves focus and moves focus to the text input', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.click(getNthReplyButton(1))
|
||||
.expect(getUrl()).contains('/reply')
|
||||
.expect(getActiveElementClass()).contains('compose-box-input')
|
||||
.click(goBackButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthStatus(0).exists).ok()
|
||||
.expect(getActiveElementClass()).contains('icon-button')
|
||||
.expect(getActiveElementAriaLabel()).eql('Reply')
|
||||
.expect(getActiveElementInsideNthStatus()).eql('1')
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
composeInput, getNthReplyButton,
|
||||
getNthStatus, getUrl, goBack
|
||||
composeInput,
|
||||
getNthComposeReplyInput, getNthReplyButton,
|
||||
getNthStatus, getUrl, goBack, homeNavButton, notificationsNavButton
|
||||
} from '../utils'
|
||||
import { foobarRole } from '../roles'
|
||||
|
||||
|
@ -10,34 +11,26 @@ fixture`017-compose-reply.js`
|
|||
test('account handle populated correctly for replies', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.click(getNthReplyButton(0))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(composeInput.value).eql('@quux ')
|
||||
.typeText(composeInput, 'hello quux', {paste: true})
|
||||
.expect(composeInput.value).eql('@quux hello quux')
|
||||
await goBack()
|
||||
await t.click(getNthReplyButton(0))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(composeInput.value).eql('@quux hello quux')
|
||||
await goBack()
|
||||
await t.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthComposeReplyInput(0).value).eql('@quux ')
|
||||
.typeText(getNthComposeReplyInput(0), 'hello quux', {paste: true})
|
||||
.expect(getNthComposeReplyInput(0).value).eql('@quux hello quux')
|
||||
.click(notificationsNavButton)
|
||||
.expect(getUrl()).contains('/notifications')
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).notContains('/notifications')
|
||||
.expect(getNthComposeReplyInput(0).value).eql('@quux hello quux')
|
||||
.expect(composeInput.value).eql('')
|
||||
await t.hover(getNthStatus(2))
|
||||
.hover(getNthStatus(2))
|
||||
.hover(getNthStatus(4))
|
||||
.click(getNthReplyButton(4))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(composeInput.value).eql('')
|
||||
await goBack()
|
||||
await t.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(composeInput.value).eql('')
|
||||
.expect(getNthComposeReplyInput(4).value).eql('')
|
||||
})
|
||||
|
||||
test('replying to posts with mentions', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.click(getNthReplyButton(1))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(composeInput.value).eql('@admin ')
|
||||
.expect(getNthComposeReplyInput(1).value).eql('@admin ')
|
||||
.navigateTo('/accounts/4')
|
||||
.click(getNthReplyButton(0))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(composeInput.value).eql('@ExternalLinks @admin @quux ')
|
||||
.expect(getNthComposeReplyInput(0).value).eql('@ExternalLinks @admin @quux ')
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { foobarRole } from '../roles'
|
||||
import {
|
||||
composeInput, getNthReplyButton, getNthStatus, getUrl, homeNavButton, notificationsNavButton,
|
||||
composeInput, getNthComposeReplyButton, getNthComposeReplyInput, getNthReplyButton, getNthStatus, getUrl,
|
||||
homeNavButton, notificationsNavButton,
|
||||
postStatusButton
|
||||
} from '../utils'
|
||||
|
||||
|
@ -27,10 +28,9 @@ test('statuses in threads show up in right order', async t => {
|
|||
.click(getNthStatus(2))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.click(getNthReplyButton(3))
|
||||
.expect(getUrl()).contains('/reply')
|
||||
.typeText(composeInput, 'my reply!', {paste: true})
|
||||
.click(postStatusButton)
|
||||
.expect(getUrl()).match(/statuses\/[^/]+$/)
|
||||
.typeText(getNthComposeReplyInput(3), 'my reply!', {paste: true})
|
||||
.click(getNthComposeReplyButton(3))
|
||||
.expect(getNthComposeReplyInput(3).exists).notOk()
|
||||
.expect(getNthStatus(5).innerText).contains('@baz my reply!')
|
||||
.navigateTo('/accounts/5')
|
||||
.click(getNthStatus(2))
|
||||
|
|
|
@ -96,6 +96,14 @@ export const uploadKittenImage = i => (exec(() => {
|
|||
}
|
||||
}))
|
||||
|
||||
export function getNthComposeReplyInput (n) {
|
||||
return getNthStatus(n).find('.compose-box-input')
|
||||
}
|
||||
|
||||
export function getNthComposeReplyButton (n) {
|
||||
return getNthStatus(n).find('.compose-box-button')
|
||||
}
|
||||
|
||||
export function getNthAutosuggestionResult (n) {
|
||||
return $(`.compose-autosuggest-list-item:nth-child(${n}) button`)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue