Merge remote-tracking branch 'origin/develop' into settings-changed

* origin/develop: (48 commits)
  fix/leftover-emoji-checkboxes-in-settings
  Apply 1 suggestion(s) to 1 file(s)
  Translated using Weblate (Spanish)
  Translated using Weblate (Persian)
  Translated using Weblate (Persian)
  Translated using Weblate (Polish)
  update changelog
  Stop click propagation when unhiding nsfw
  Fix Follow Requests title style
  Translated using Weblate (Persian)
  Translated using Weblate (Persian)
  Translated using Weblate (French)
  Added translation using Weblate (Persian)
  Translated using Weblate (Chinese (Traditional))
  Translated using Weblate (Chinese (Simplified))
  Translated using Weblate (Italian)
  Translated using Weblate (English)
  Translated using Weblate (English)
  Translated using Weblate (Basque)
  Translated using Weblate (Spanish)
  ...
This commit is contained in:
Henry Jameson 2020-10-17 19:24:07 +03:00
commit 29ff0be92c
45 changed files with 1774 additions and 139 deletions

View file

@ -28,7 +28,7 @@
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@click.prevent="toggleHidden"
@click.prevent.stop="toggleHidden"
>
<img
:key="nsfwImage"
@ -80,6 +80,8 @@
class="video"
:attachment="attachment"
:controls="allowPlay"
@play="$emit('play')"
@pause="$emit('pause')"
/>
<i
v-if="!allowPlay"
@ -93,6 +95,8 @@
:alt="attachment.description"
:title="attachment.description"
controls
@play="$emit('play')"
@pause="$emit('pause')"
/>
<div

View file

@ -5,6 +5,7 @@ import ChatMessage from '../chat_message/chat_message.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
const BOTTOMED_OUT_OFFSET = 10
@ -246,7 +247,7 @@ const Chat = {
const fetchOlderMessages = !!maxId
const sinceId = fetchLatest && chatMessageService.maxId
this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
.then((messages) => {
// Clear the current chat in case we're recovering from a ws connection loss.
if (isFirstFetch) {
@ -287,7 +288,7 @@ const Chat = {
},
doStartFetching () {
this.$store.dispatch('startFetchingCurrentChat', {
fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
})
this.fetchChat({ isFirstFetch: true })
},

View file

@ -44,7 +44,8 @@ const conversation = {
'isPage',
'pinnedStatusIdsObject',
'inProfile',
'profileUserId'
'profileUserId',
'virtualHidden'
],
created () {
if (this.isPage) {
@ -52,6 +53,13 @@ const conversation = {
}
},
computed: {
hideStatus () {
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
} else {
return this.virtualHidden
}
},
status () {
return this.$store.state.statuses.allStatusesObject[this.statusId]
},
@ -102,6 +110,10 @@ const conversation = {
},
isExpanded () {
return this.expanded || this.isPage
},
hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {}
}
},
components: {
@ -121,6 +133,12 @@ const conversation = {
if (value) {
this.fetchConversation()
}
},
virtualHidden (value) {
this.$store.dispatch(
'setVirtualHeight',
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
)
}
},
methods: {

View file

@ -1,5 +1,7 @@
<template>
<div
v-if="!hideStatus"
:style="hiddenStyle"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
>
@ -18,6 +20,7 @@
<status
v-for="status in conversation"
:key="status.id"
ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
@ -33,6 +36,10 @@
@toggleExpanded="toggleExpanded"
/>
</div>
<div
v-else
:style="hiddenStyle"
/>
</template>
<script src="./conversation.js"></script>
@ -53,8 +60,8 @@
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left: 4px solid $fallback--cRed;
border-left: 4px solid var(--cRed, $fallback--cRed);
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
}
.conversation-status:last-child {

View file

@ -1,7 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
{{ $t('nav.friend_requests') }}
<div class="title">
{{ $t('nav.friend_requests') }}
</div>
</div>
<div class="panel-body">
<FollowRequestCard

View file

@ -178,7 +178,7 @@
box-shadow: var(--inputShadow);
&.menu-checkbox-checked::after {
content: '';
content: '';
}
}

View file

@ -7,12 +7,12 @@
:to="{ name: timelinesRoute }"
:class="onTimelineRoute && 'router-link-active'"
>
<i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }}
<i class="button-icon icon-home-2" />{{ $t("nav.timelines") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
<i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
<i class="button-icon icon-bell-alt" />{{ $t("nav.interactions") }}
</router-link>
</li>
<li v-if="currentUser && pleromaChatMessagesAvailable">
@ -23,12 +23,12 @@
>
{{ unreadChatCount }}
</div>
<i class="button-icon icon-chat" /> {{ $t("nav.chats") }}
<i class="button-icon icon-chat" />{{ $t("nav.chats") }}
</router-link>
</li>
<li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }">
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
<i class="button-icon icon-user-plus" />{{ $t("nav.friend_requests") }}
<span
v-if="followRequestCount > 0"
class="badge follow-request-count"
@ -39,7 +39,7 @@
</li>
<li>
<router-link :to="{ name: 'about' }">
<i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
<i class="button-icon icon-info-circled" />{{ $t("nav.about") }}
</router-link>
</li>
</ul>
@ -125,6 +125,10 @@
}
}
.nav-panel .button-icon {
margin-right: 0.5em;
}
.nav-panel .button-icon:before {
width: 1.1em;
}

View file

@ -1,5 +1,4 @@
import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status'],
@ -35,7 +34,9 @@ const ReactButton = {
}
return this.$store.state.instance.emoji || []
},
...mapGetters(['mergedConfig'])
mergedConfig () {
return this.$store.getters.mergedConfig
}
}
}

View file

@ -1,4 +1,3 @@
import { mapGetters } from 'vuex'
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
@ -28,7 +27,9 @@ const RetweetButton = {
'animate-spin': this.animated
}
},
...mapGetters(['mergedConfig'])
mergedConfig () {
return this.$store.getters.mergedConfig
}
}
}

View file

@ -1,6 +1,7 @@
import Importer from 'src/components/importer/importer.vue'
import Exporter from 'src/components/exporter/exporter.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { mapState } from 'vuex'
const DataImportExportTab = {
data () {
@ -18,21 +19,26 @@ const DataImportExportTab = {
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
}
...mapState({
backendInteractor: (state) => state.api.backendInteractor,
user: (state) => state.users.currentUser
})
},
methods: {
getFollowsContent () {
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
return this.backendInteractor.exportFriends({ id: this.user.id })
.then(this.generateExportableUsersContent)
},
getBlocksContent () {
return this.$store.state.api.backendInteractor.fetchBlocks()
return this.backendInteractor.fetchBlocks()
.then(this.generateExportableUsersContent)
},
getMutesContent () {
return this.backendInteractor.fetchMutes()
.then(this.generateExportableUsersContent)
},
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
return this.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@ -40,7 +46,15 @@ const DataImportExportTab = {
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks({ file })
return this.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
importMutes (file) {
return this.backendInteractor.importMutes({ file })
.then((status) => {
if (!status) {
throw new Error('failed')

View file

@ -36,6 +36,23 @@
:export-button-label="$t('settings.block_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_import') }}</h2>
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
<Importer
:submit-handler="importMutes"
:success-message="$t('settings.mutes_imported')"
:error-message="$t('settings.mute_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_export') }}</h2>
<Exporter
:get-content="getMutesContent"
filename="mutes.csv"
:export-button-label="$t('settings.mute_export_button')"
/>
</div>
</div>
</template>

View file

@ -58,6 +58,11 @@
{{ $t('settings.emoji_reactions_on_timeline') }}
</BooleanSetting>
</li>
<li>
<Checkbox v-model="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</Checkbox>
</li>
</ul>
</div>

View file

@ -15,7 +15,6 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
import { mapGetters, mapState } from 'vuex'
const Status = {
name: 'Status',
@ -54,6 +53,8 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
mediaPlaying: [],
suspendable: true,
error: null
}
},
@ -157,7 +158,7 @@ const Status = {
return this.mergedConfig.hideFilteredStatuses
},
hideStatus () {
return this.deleted || (this.muted && this.hideFilteredStatuses)
return this.deleted || (this.muted && this.hideFilteredStatuses) || this.virtualHidden
},
isFocused () {
// retweet or root of an expanded conversation
@ -207,11 +208,18 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
currentUser () {
return this.$store.state.users.currentUser
},
betterShadow () {
return this.$store.state.interface.browserSupport.cssFilter
},
mergedConfig () {
return this.$store.getters.mergedConfig
},
isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0
}
},
methods: {
visibilityIcon (visibility) {
@ -251,6 +259,12 @@ const Status = {
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
addMediaPlaying (id) {
this.mediaPlaying.push(id)
},
removeMediaPlaying (id) {
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
}
},
watch: {
@ -280,6 +294,9 @@ const Status = {
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
this.$store.dispatch('fetchFavs', this.status.id)
}
},
'isSuspendable': function (val) {
this.suspendable = val
}
},
filters: {

View file

@ -25,6 +25,11 @@ $status-margin: 0.75em;
--icon: var(--selectedPostIcon, $fallback--icon);
}
&.-conversation {
border-left-width: 4px;
border-left-style: solid;
}
.status-container {
display: flex;
padding: $status-margin;

View file

@ -16,7 +16,7 @@
/>
</div>
<template v-if="muted && !isPreview">
<div class="status-csontainer muted">
<div class="status-container muted">
<small class="status-username">
<i
v-if="muted && retweet"
@ -227,6 +227,7 @@
</span>
</a>
</StatusPopover>
<span
v-else
class="reply-to-no-popover"
@ -272,6 +273,8 @@
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
/>
<transition name="fade">
@ -354,6 +357,7 @@
@onSuccess="clearError"
/>
</div>
</div>
</div>
<div
@ -376,4 +380,5 @@
</template>
<script src="./status.js" ></script>
<style src="./status.scss" lang="scss"></style>

View file

@ -107,6 +107,8 @@
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"

View file

@ -19,14 +19,16 @@ const StillImage = {
},
methods: {
onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const image = this.$refs.src
if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image)
const canvas = this.$refs.canvas
if (!canvas) return
const width = this.$refs.src.naturalWidth
const height = this.$refs.src.naturalHeight
const width = image.naturalWidth
const height = image.naturalHeight
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
},
onError () {
this.imageLoadError && this.imageLoadError()

View file

@ -33,7 +33,8 @@ const Timeline = {
return {
paused: false,
unfocused: false,
bottomedOut: false
bottomedOut: false,
virtualScrollIndex: 0
}
},
components: {
@ -78,6 +79,16 @@ const Timeline = {
},
pinnedStatusIdsObject () {
return keyBy(this.pinnedStatusIds)
},
statusesToDisplay () {
const amount = this.timeline.visibleStatuses.length
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
},
virtualScrollingEnabled () {
return this.$store.getters.mergedConfig.virtualScrolling
}
},
created () {
@ -85,7 +96,7 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.scrollLoad)
window.addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) { return false }
@ -104,9 +115,10 @@ const Timeline = {
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
setTimeout(this.determineVisibleStatuses, 250)
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
@ -146,6 +158,48 @@ const Timeline = {
}
})
}, 1000, this),
determineVisibleStatuses () {
if (!this.$refs.timeline) return
if (!this.virtualScrollingEnabled) return
const statuses = this.$refs.timeline.children
const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1))
if (statuses.length === 0) return
const height = Math.max(document.body.offsetHeight, window.pageYOffset)
const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
// Start from approximating the index of some visible status by using the
// the center of the screen on the timeline.
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's
// closer than the previous approximation, use it if so
const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y
if (Math.abs(err) > virtualScrollIndexY) {
approxIndex = cappedScrollIndex
err = virtualScrollIndexY
}
// if the status is too far from viewport, check the next/previous ones if
// they happen to be better
while (err < -20 && approxIndex < statuses.length - 1) {
err += statuses[approxIndex].offsetHeight
approxIndex++
}
while (err > window.innerHeight + 100 && approxIndex > 0) {
approxIndex--
err -= statuses[approxIndex].offsetHeight
}
// this status is now the center point for virtual scrolling and visible
// statuses will be nearby statuses before and after it
this.virtualScrollIndex = approxIndex
},
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
@ -155,6 +209,10 @@ const Timeline = {
this.fetchOlderStatuses()
}
},
handleScroll: throttle(function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 200),
handleVisibilityChange () {
this.unfocused = document.hidden
}

View file

@ -32,7 +32,10 @@
</div>
</div>
<div :class="classes.body">
<div class="timeline">
<div
ref="timeline"
class="timeline"
>
<template v-for="statusId in pinnedStatusIds">
<conversation
v-if="timeline.statusesObject[statusId]"
@ -54,6 +57,7 @@
:collapsable="true"
:in-profile="inProfile"
:profile-user-id="userId"
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
/>
</template>
</div>

View file

@ -138,15 +138,11 @@
&:last-child {
border: none;
}
i {
margin: 0 0.5em;
}
}
a {
display: block;
padding: 0.6em 0;
padding: 0.6em 0.65em;
&:hover {
background-color: $fallback--lightBg;
@ -174,6 +170,10 @@
text-decoration: underline;
}
}
i {
margin-right: 0.5em;
}
}
}

View file

@ -156,8 +156,7 @@
.user-profile-field {
display: flex;
margin: 0.25em auto;
max-width: 32em;
margin: 0.25em;
border: 1px solid var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);

View file

@ -3,27 +3,48 @@ const VideoAttachment = {
props: ['attachment', 'controls'],
data () {
return {
loopVideo: this.$store.getters.mergedConfig.loopVideo
blocksSuspend: false,
// Start from true because removing "loop" property seems buggy in Vue
hasAudio: true
}
},
computed: {
loopVideo () {
if (this.$store.getters.mergedConfig.loopVideoSilentOnly) {
return !this.hasAudio
}
return this.$store.getters.mergedConfig.loopVideo
}
},
methods: {
onVideoDataLoad (e) {
onPlaying (e) {
this.setHasAudio(e)
if (this.loopVideo) {
this.$emit('play', { looping: true })
return
}
this.$emit('play')
},
onPaused (e) {
this.$emit('pause')
},
setHasAudio (e) {
const target = e.srcElement || e.target
// If hasAudio is false, we've already marked this video to not have audio,
// a video can't gain audio out of nowhere so don't bother checking again.
if (!this.hasAudio) return
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track
if (target.webkitAudioDecodedByteCount > 0) {
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) {
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) {
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
if (target.webkitAudioDecodedByteCount > 0) return
}
if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) return
}
if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) return
}
this.hasAudio = false
}
}
}

View file

@ -7,7 +7,8 @@
:alt="attachment.description"
:title="attachment.description"
playsinline
@loadeddata="onVideoDataLoad"
@playing="onPlaying"
@pause="onPaused"
/>
</template>