Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (32 commits)
  set flex-shrink and flex-basis explicitly
  set flex amount correctly
  update flex-grow calculation logic
  keep image natural ratio in gallery row
  populate gallery row height without getting width
  refactor using Set
  clean up
  update event name
  update condition
  move modal animation keyframes definition
  migrate viewClass prop to class attribute
  fix eslint errors
  fix message input not auto-focusing bug
  revert changes to render modal into portal
  use higher css specificity
  fix eslint warnings
  remove needless ref definition
  render modals into the “modal” portal
  remove needless console.log
  remove needless importing
  ...
This commit is contained in:
Henry Jameson 2019-10-27 19:28:50 +02:00
commit 6a5d57d2fe
24 changed files with 207 additions and 176 deletions

View file

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased] ## [Unreleased]
### Added ### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker - Emoji picker
- Started changelog anew - Started changelog anew
### Changed ### Changed

View file

@ -717,31 +717,6 @@ nav {
} }
} }
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
}
.button-icon { .button-icon {
font-size: 1.2em; font-size: 1.2em;
} }

View file

@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },

View file

@ -10,7 +10,8 @@ const Attachment = {
'statusId', 'statusId',
'size', 'size',
'allowPlay', 'allowPlay',
'setMedia' 'setMedia',
'naturalSizeLoad'
], ],
data () { data () {
return { return {
@ -88,6 +89,11 @@ const Attachment = {
} else { } else {
this.showHidden = !this.showHidden this.showHidden = !this.showHidden
} }
},
onImageLoad (image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
} }
} }
} }

View file

@ -58,6 +58,7 @@
:referrerpolicy="referrerpolicy" :referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype" :mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
/> />
</a> </a>

View file

@ -1,6 +1,6 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['user'], props: ['user', 'labelFollowing', 'buttonClass'],
data () { data () {
return { return {
inProgress: false inProgress: false
@ -23,7 +23,7 @@ export default {
if (this.inProgress) { if (this.inProgress) {
return this.$t('user_card.follow_progress') return this.$t('user_card.follow_progress')
} else if (this.user.following) { } else if (this.user.following) {
return this.$t('user_card.following') return this.labelFollowing || this.$t('user_card.following')
} else if (this.user.requested) { } else if (this.user.requested) {
return this.$t('user_card.follow_sent') return this.$t('user_card.follow_sent')
} else { } else {

View file

@ -1,20 +1,16 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import FollowButton from '../follow_button/follow_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: [
'user', 'user',
'noFollowsYou' 'noFollowsYou'
], ],
data () {
return {
inProgress: false
}
},
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow RemoteFollow,
FollowButton
}, },
computed: { computed: {
isMe () { isMe () {
@ -23,20 +19,6 @@ const FollowCard = {
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
} }
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
this.inProgress = false
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false
})
}
} }
} }

View file

@ -16,36 +16,11 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<button <FollowButton
v-if="!user.following" :user="user"
class="btn btn-default follow-card-follow-button" class="follow-card-follow-button"
:disabled="inProgress" :label-following="$t('user_card.follow_unfollow')"
:title="user.requested ? $t('user_card.follow_again') : ''" />
@click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="user.requested">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button
v-else
class="btn btn-default follow-card-follow-button pressed"
:disabled="inProgress"
@click="unfollowUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</template> </template>
</div> </div>
</basic-user-card> </basic-user-card>

View file

@ -1,23 +1,18 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight } from 'lodash' import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = { const Gallery = {
data: () => ({
width: 500
}),
props: [ props: [
'attachments', 'attachments',
'nsfw', 'nsfw',
'setMedia' 'setMedia'
], ],
data () {
return {
sizes: {}
}
},
components: { Attachment }, components: { Attachment },
mounted () {
this.resize()
window.addEventListener('resize', this.resize)
},
destroyed () {
window.removeEventListener('resize', this.resize)
},
computed: { computed: {
rows () { rows () {
if (!this.attachments) { if (!this.attachments) {
@ -33,21 +28,24 @@ const Gallery = {
} }
return rows return rows
}, },
rowHeight () {
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
},
useContainFit () { useContainFit () {
return this.$store.getters.mergedConfig.useContainFit return this.$store.getters.mergedConfig.useContainFit
} }
}, },
methods: { methods: {
resize () { onNaturalSizeLoad (id, size) {
// Quick optimization to make resizing not always trigger state change, this.$set(this.sizes, id, size)
// only update attachment size in 10px steps },
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10 rowStyle (itemsPerRow) {
if (this.width !== width) { return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
this.width = width },
} itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
},
getAspectRatio (id) {
const size = this.sizes[id]
return size ? size.width / size.height : 1
} }
} }
} }

View file

@ -7,9 +7,10 @@
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"
class="gallery-row" class="gallery-row"
:style="rowHeight(row.length)" :style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
> >
<div class="gallery-row-inner">
<attachment <attachment
v-for="attachment in row" v-for="attachment in row"
:key="attachment.id" :key="attachment.id"
@ -17,9 +18,12 @@
:nsfw="nsfw" :nsfw="nsfw"
:attachment="attachment" :attachment="attachment"
:allow-play="false" :allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script src='./gallery.js'></script> <script src='./gallery.js'></script>
@ -28,14 +32,23 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.gallery-row { .gallery-row {
height: 200px; position: relative;
height: 0;
width: 100%; width: 100%;
flex-grow: 1;
margin-top: 0.5em;
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-content: stretch; align-content: stretch;
flex-grow: 1; }
margin-top: 0.5em;
// FIXME: specificity problem with this and .attachments.attachment // FIXME: specificity problem with this and .attachments.attachment
// we shouldn't have the need for .image here // we shouldn't have the need for .image here

View file

@ -59,6 +59,8 @@ const LoginForm = {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result }) this.requireMFA({ app: app, settings: result })
} else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else { } else {
this.error = result.error this.error = result.error
this.focusOnPasswordInput() this.focusOnPasswordInput()

View file

@ -1,11 +1,13 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
const MediaModal = { const MediaModal = {
components: { components: {
StillImage, StillImage,
VideoAttachment VideoAttachment,
Modal
}, },
computed: { computed: {
showing () { showing () {

View file

@ -1,9 +1,8 @@
<template> <template>
<div <Modal
v-if="showing" v-if="showing"
v-body-scroll-lock="showing" class="media-modal-view"
class="modal-view media-modal-view" @backdropClicked="hide"
@click.prevent="hide"
> >
<img <img
v-if="type === 'image'" v-if="type === 'image'"
@ -33,21 +32,15 @@
> >
<i class="icon-right-open arrow-icon" /> <i class="icon-right-open arrow-icon" />
</button> </button>
</div> </Modal>
</template> </template>
<script src="./media_modal.js"></script> <script src="./media_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.media-modal-view {
.media-modal-view {
z-index: 1001; z-index: 1001;
body:not(.scroll-locked) & {
display: none;
}
&:hover { &:hover {
.modal-view-button-arrow { .modal-view-button-arrow {
opacity: 0.75; opacity: 0.75;
@ -114,5 +107,4 @@
} }
} }
} }
</style> </style>

View file

@ -0,0 +1,52 @@
<template>
<div
v-show="isOpen"
v-body-scroll-lock="isOpen"
class="modal-view"
@click.self="$emit('backdropClicked')"
>
<slot />
</div>
</template>
<script>
export default {
props: {
isOpen: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss">
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
body:not(.scroll-locked) & {
opacity: 0;
}
}
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
</style>

View file

@ -25,6 +25,12 @@ const passwordReset = {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
}, },
props: {
passwordResetRequested: {
default: false,
type: Boolean
}
},
methods: { methods: {
dismissError () { dismissError () {
this.error = null this.error = null

View file

@ -10,7 +10,10 @@
> >
<div class="container"> <div class="container">
<div v-if="!mailerEnabled"> <div v-if="!mailerEnabled">
<p> <p v-if="passwordResetRequested">
{{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }}
</p>
<p v-else>
{{ $t('password_reset.password_reset_disabled') }} {{ $t('password_reset.password_reset_disabled') }}
</p> </p>
</div> </div>
@ -25,6 +28,12 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<p
v-if="passwordResetRequested"
class="password-reset-required error"
>
{{ $t('password_reset.password_reset_required') }}
</p>
<p> <p>
{{ $t('password_reset.instruction') }} {{ $t('password_reset.instruction') }}
</p> </p>
@ -104,6 +113,11 @@
margin: 0.3em 0.0em 1em; margin: 0.3em 0.0em 1em;
} }
.password-reset-required {
background-color: var(--alertError, $fallback--alertError);
padding: 10px 0;
}
.notice-dismissible { .notice-dismissible {
padding-right: 2rem; padding-right: 2rem;
} }

View file

@ -1,9 +1,11 @@
import PostStatusForm from '../post_status_form/post_status_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import get from 'lodash/get' import get from 'lodash/get'
const PostStatusModal = { const PostStatusModal = {
components: { components: {
PostStatusForm PostStatusForm,
Modal
}, },
data () { data () {
return { return {

View file

@ -1,14 +1,11 @@
<template> <template>
<div <Modal
v-if="isLoggedIn && !resettingForm" v-if="isLoggedIn && !resettingForm"
v-show="modalActivated" :is-open="modalActivated"
class="post-form-modal-view modal-view" class="post-form-modal-view"
@click="closeModal" @backdropClicked="closeModal"
>
<div
class="post-form-modal-panel panel"
@click.stop=""
> >
<div class="post-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
{{ $t('post_status.new_status') }} {{ $t('post_status.new_status') }}
</div> </div>
@ -18,15 +15,13 @@
@posted="closeModal" @posted="closeModal"
/> />
</div> </div>
</div> </Modal>
</template> </template>
<script src="./post_status_modal.js"></script> <script src="./post_status_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.post-form-modal-view {
.post-form-modal-view {
align-items: flex-start; align-items: flex-start;
} }

View file

@ -840,6 +840,11 @@ $status-margin: 0.75em;
&.button-icon-active { &.button-icon-active {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer; cursor: pointer;
} }
} }

View file

@ -3,7 +3,8 @@ const StillImage = {
'src', 'src',
'referrerpolicy', 'referrerpolicy',
'mimetype', 'mimetype',
'imageLoadError' 'imageLoadError',
'imageLoadHandler'
], ],
data () { data () {
return { return {
@ -17,6 +18,7 @@ const StillImage = {
}, },
methods: { methods: {
onLoad () { onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (!canvas) return if (!canvas) return
const width = this.$refs.src.naturalWidth const width = this.$refs.src.naturalWidth

View file

@ -2,12 +2,14 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Modal from '../modal/modal.vue'
const UserReportingModal = { const UserReportingModal = {
components: { components: {
Status, Status,
List, List,
Checkbox Checkbox,
Modal
}, },
data () { data () {
return { return {

View file

@ -1,13 +1,9 @@
<template> <template>
<div <Modal
v-if="isOpen" v-if="isOpen"
class="modal-view" @backdropClicked="closeModal"
@click="closeModal"
>
<div
class="user-reporting-panel panel"
@click.stop=""
> >
<div class="user-reporting-panel panel">
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <div class="title">
{{ $t('user_reporting.title', [user.screen_name]) }} {{ $t('user_reporting.title', [user.screen_name]) }}
@ -69,7 +65,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </Modal>
</template> </template>
<script src="./user_reporting_modal.js"></script> <script src="./user_reporting_modal.js"></script>

View file

@ -2,13 +2,16 @@ import * as bodyScrollLock from 'body-scroll-lock'
let previousNavPaddingRight let previousNavPaddingRight
let previousAppBgWrapperRight let previousAppBgWrapperRight
const lockerEls = new Set([])
const disableBodyScroll = (el) => { const disableBodyScroll = (el) => {
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
bodyScrollLock.disableBodyScroll(el, { bodyScrollLock.disableBodyScroll(el, {
reserveScrollBarGap: true reserveScrollBarGap: true
}) })
lockerEls.add(el)
setTimeout(() => { setTimeout(() => {
if (lockerEls.size <= 1) {
// If previousNavPaddingRight is already set, don't set it again. // If previousNavPaddingRight is already set, don't set it again.
if (previousNavPaddingRight === undefined) { if (previousNavPaddingRight === undefined) {
const navEl = document.getElementById('nav') const navEl = document.getElementById('nav')
@ -22,11 +25,14 @@ const disableBodyScroll = (el) => {
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
} }
document.body.classList.add('scroll-locked') document.body.classList.add('scroll-locked')
}
}) })
} }
const enableBodyScroll = (el) => { const enableBodyScroll = (el) => {
lockerEls.delete(el)
setTimeout(() => { setTimeout(() => {
if (lockerEls.size === 0) {
if (previousNavPaddingRight !== undefined) { if (previousNavPaddingRight !== undefined) {
document.getElementById('nav').style.paddingRight = previousNavPaddingRight document.getElementById('nav').style.paddingRight = previousNavPaddingRight
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
@ -38,6 +44,7 @@ const enableBodyScroll = (el) => {
previousAppBgWrapperRight = undefined previousAppBgWrapperRight = undefined
} }
document.body.classList.remove('scroll-locked') document.body.classList.remove('scroll-locked')
}
}) })
bodyScrollLock.enableBodyScroll(el) bodyScrollLock.enableBodyScroll(el)
} }

View file

@ -635,6 +635,8 @@
"return_home": "Return to the home page", "return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.", "not_found": "We couldn't find that email or username.",
"too_many_requests": "You have reached the limit of attempts, try again later.", "too_many_requests": "You have reached the limit of attempts, try again later.",
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
"password_reset_required": "You must reset your password to log in.",
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
} }
} }