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

* upstream/develop: (29 commits)
  add hideISP to defaultState of config module
  add changelog entry
  mrf transparency panel: refactor to use vuex mapState
  mrf transparency panel: remove unneeded components{}
  boot: cleanup resolveStaffAccounts
  lint
  about: add MRF transparency panel
  about: add staff panel
  about page: fix hiding of instance-specific panel, flow ToS and ISP better
  nav panel: add link to about page
  redirect /remote-users/:username@:hostname -> /users/:id, /remote-users/:hostname/:username -> /users/:id
  clear filter on reopen, fix error message in console
  reset position when reopening emoji picker
  eslint fix
  fix not being able to see unicode emojis when there few of them when searching on emoji-a-ton instances
  replace sanity button with loading on scroll
  fix search not working, use computer property instead of state
  fix eslint warnings
  Lightbox/modal multi image improvements - #381
  '/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa'
  ...
This commit is contained in:
Henry Jameson 2019-11-11 00:26:13 +02:00
commit 45a1d30bd6
31 changed files with 590 additions and 105 deletions

View file

@ -1,15 +1,24 @@
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from '../features_panel/features_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
const About = {
components: {
InstanceSpecificPanel,
FeaturesPanel,
TermsOfServicePanel
TermsOfServicePanel,
StaffPanel,
MRFTransparencyPanel
},
computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
}
}
}

View file

@ -1,8 +1,10 @@
<template>
<div class="sidebar">
<instance-specific-panel />
<features-panel v-if="showFeaturesPanel" />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
<MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" />
</div>
</template>

View file

@ -2,7 +2,7 @@
<label
class="checkbox"
:class="{ disabled, indeterminate }"
>
>
<input
type="checkbox"
:disabled="disabled"
@ -12,9 +12,9 @@
>
<i class="checkbox-indicator" />
<span
class="label"
v-if="!!$slots.default"
>
class="label"
>
<slot />
</span>
</label>

View file

@ -1,9 +1,12 @@
import Checkbox from '../checkbox/checkbox.vue'
import { set } from 'vue'
const LOAD_EMOJI_BY = 50
const LOAD_EMOJI_INTERVAL = 100
const LOAD_EMOJI_SANE_AMOUNT = 500
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword))
@ -24,10 +27,8 @@ const EmojiPicker = {
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBuffer: (this.$store.state.instance.customEmoji || [])
.slice(0, LOAD_EMOJI_BY),
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiCounter: LOAD_EMOJI_BY,
customEmojiLoadAllConfirmed: false
}
},
@ -36,10 +37,22 @@ const EmojiPicker = {
Checkbox
},
methods: {
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
highlight (key) {
const ref = this.$refs['group-' + key]
const top = ref[0].offsetTop
@ -49,9 +62,7 @@ const EmojiPicker = {
this.$refs['emoji-groups'].scrollTop = top + 1
})
},
scrolledGroup (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
const top = target.scrollTop + 5
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
@ -59,6 +70,28 @@ const EmojiPicker = {
} else {
this.groupsScrolledClass = 'scrolled-middle'
}
},
triggerLoadMore (target) {
const ref = this.$refs['group-end-custom'][0]
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
@ -68,67 +101,40 @@ const EmojiPicker = {
})
})
},
loadEmojiInsane () {
this.customEmojiLoadAllConfirmed = true
this.continueEmojiLoad()
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
if (allLoaded || saneLoaded) {
if (allLoaded) {
return
}
this.customEmojiBuffer.push(
...this.filteredEmoji.slice(
this.customEmojiCounter,
this.customEmojiCounter + LOAD_EMOJI_BY
)
)
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
this.customEmojiCounter += LOAD_EMOJI_BY
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledSane = bufferSize === LOAD_EMOJI_SANE_AMOUNT && !this.customEmojiLoadAllConfirmed
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (forceUpdate || bufferPrefilledSane || bufferPrefilledAll) {
if (bufferPrefilledAll && !forceUpdate) {
return
}
if (this.customEmojiTimeout) {
window.clearTimeout(this.customEmojiTimeout)
}
set(
this,
'customEmojiBuffer',
this.filteredEmoji.slice(0, LOAD_EMOJI_BY)
)
this.customEmojiCounter = LOAD_EMOJI_BY
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
},
continueEmojiLoad () {
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
this.customEmojiBufferSlice = LOAD_EMOJI_BY
},
toggleStickers () {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
this.showingStickers = value
},
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.scrolledGroup()
this.onScroll()
this.startEmojiLoad(true)
}
},
@ -142,19 +148,14 @@ const EmojiPicker = {
}
return 0
},
saneAmount () {
// for UI
return LOAD_EMOJI_SANE_AMOUNT
},
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
this.keyword
)
},
askForSanity () {
return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []

View file

@ -47,7 +47,7 @@
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="scrolledGroup"
@scroll="onScroll"
>
<div
v-for="group in emojisView"
@ -73,6 +73,7 @@
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
@ -80,20 +81,6 @@
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
<div
v-if="askForSanity"
class="too-many-emoji"
>
<div class="alert warning hint">
{{ $t('emoji.load_all_hint', { saneAmount } ) }}
</div>
<button
class="btn btn-default"
@click.prevent="loadEmojiInsane"
>
{{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
</button>
</div>
</div>
<div
v-if="showingStickers"

View file

@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.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 GestureService from '../../services/gesture_service/gesture_service'
const MediaModal = {
components: {
@ -29,7 +30,27 @@ const MediaModal = {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
}
},
created () {
this.mediaSwipeGestureRight = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT,
this.goPrev,
50
)
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
GestureService.DIRECTION_LEFT,
this.goNext,
50
)
},
methods: {
mediaTouchStart (e) {
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
},
mediaTouchMove (e) {
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
},
hide () {
this.$store.dispatch('closeMediaViewer')
},

View file

@ -8,6 +8,8 @@
v-if="type === 'image'"
class="modal-image"
:src="currentMedia.url"
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
>
<VideoAttachment
v-if="type === 'video'"
@ -41,18 +43,16 @@
.modal-view.media-modal-view {
z-index: 1001;
&:hover {
.modal-view-button-arrow {
opacity: 0.75;
.modal-view-button-arrow {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
}

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
const MRFTransparencyPanel = {
computed: mapState({
federationPolicy: state => state.instance.federationPolicy,
mrfPolicies: state => state.instance.federationPolicy.mrf_policies,
acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept,
rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject,
quarantineInstances: state => state.instance.federationPolicy.quarantined_instances,
ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal,
mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw,
mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal
})
}
export default MRFTransparencyPanel

View file

@ -0,0 +1,122 @@
<template>
<div
v-if="federationPolicy"
class="mrf-transparency-panel"
>
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
{{ $t("about.federation") }}
</div>
</div>
<div class="panel-body">
<div class="mrf-section">
<h2>{{ $t("about.mrf_policies") }}</h2>
<p>{{ $t("about.mrf_policies_desc") }}</p>
<ul>
<li
v-for="policy in mrfPolicies"
:key="policy"
v-text="policy"
/>
</ul>
<h2>{{ $t("about.mrf_policy_simple") }}</h2>
<div v-if="acceptInstances.length">
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
<p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
<ul>
<li
v-for="instance in acceptInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="rejectInstances.length">
<h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
<p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
<ul>
<li
v-for="instance in rejectInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="quarantineInstances.length">
<h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
<p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
<ul>
<li
v-for="instance in quarantineInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="ftlRemovalInstances.length">
<h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
<p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
<ul>
<li
v-for="instance in ftlRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="mediaNsfwInstances.length">
<h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
<p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
<ul>
<li
v-for="instance in mediaNsfwInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="mediaRemovalInstances.length">
<h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
<p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
<ul>
<li
v-for="instance in mediaRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="./mrf_transparency_panel.js"></script>
<style lang="scss">
.mrf-section {
margin: 1em;
}
</style>

View file

@ -38,6 +38,11 @@
{{ $t("nav.twkn") }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'about' }">
{{ $t("nav.about") }}
</router-link>
</li>
</ul>
</div>
</div>

View file

@ -163,7 +163,7 @@
<div
ref="bottom"
class="form-bottom"
>
>
<div class="form-bottom-left">
<media-upload
ref="mediaUpload"

View file

@ -0,0 +1,31 @@
const RemoteUserResolver = {
data: () => ({
error: false
}),
mounted () {
this.redirect()
},
methods: {
redirect () {
const acct = this.$route.params.username + '@' + this.$route.params.hostname
this.$store.state.api.backendInteractor.fetchUser({ id: acct })
.then((externalUser) => {
if (externalUser.error) {
this.error = true
} else {
this.$store.commit('addNewUsers', [externalUser])
const id = externalUser.id
this.$router.replace({
name: 'external-user-profile',
params: { id }
})
}
})
.catch(() => {
this.error = true
})
}
}
}
export default RemoteUserResolver

View file

@ -0,0 +1,20 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{ $t('remote_user_resolver.remote_user_resolver') }}
</div>
<div class="panel-body">
<p>
{{ $t('remote_user_resolver.searching_for') }} @{{ $route.params.username }}@{{ $route.params.hostname }}
</p>
<p v-if="error">
{{ $t('remote_user_resolver.error') }}
</p>
</div>
</div>
</template>
<script src="./remote_user_resolver.js"></script>
<style lang="scss">
</style>

View file

@ -0,0 +1,14 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
components: {
BasicUserCard
},
computed: {
staffAccounts () {
return this.$store.state.instance.staffAccounts
}
}
}
export default StaffPanel

View file

@ -0,0 +1,23 @@
<template>
<div class="staff-panel">
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
{{ $t("about.staff") }}
</div>
</div>
<div class="panel-body">
<basic-user-card
v-for="user in staffAccounts"
:key="user.screen_name"
:user="user"
/>
</div>
</div>
</div>
</template>
<script src="./staff_panel.js" ></script>
<style lang="scss">
</style>

View file

@ -12,11 +12,13 @@ export default Vue.component('tab-switcher', {
},
onSwitch: {
required: false,
type: Function
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String
type: String,
default: undefined
},
scrollableTabs: {
required: false,

View file

@ -276,6 +276,8 @@
mask-composite: exclude;
background-size: cover;
mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
&.hide-bio {
mask-size: 100% 40px;

View file

@ -35,6 +35,7 @@ const MuteList = withSubscription({
const UserSettings = {
data () {
return {
newEmail: '',
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
@ -56,6 +57,9 @@ const UserSettings = {
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
changeEmailError: false,
changeEmailPassword: '',
changedEmail: false,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
@ -305,6 +309,22 @@ const UserSettings = {
}
})
},
changeEmail () {
const params = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
this.changeEmailError = false
} else {
this.changedEmail = false
this.changeEmailError = res.error
}
})
},
activateTab (tabName) {
this.activeTab = tabName
},

View file

@ -85,14 +85,14 @@
<Checkbox
v-model="hideFollowsCount"
:disabled="!hideFollows"
>
>
{{ $t('settings.hide_follows_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox
v-model="hideFollowers"
>
>
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
@ -233,6 +233,39 @@
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2>
<div>
<p>{{ $t('settings.new_email') }}</p>
<input
v-model="newEmail"
type="email"
autocomplete="email"
>
</div>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
>
</div>
<button
class="btn btn-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedEmail">
{{ $t('settings.changed_email') }}
</p>
<template v-if="changeEmailError !== false">
<p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p>
</template>
</div>
<div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2>
<div>