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:
commit
29ff0be92c
45 changed files with 1774 additions and 139 deletions
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
box-shadow: var(--inputShadow);
|
||||
|
||||
&.menu-checkbox-checked::after {
|
||||
content: '✔';
|
||||
content: '✓';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
playsinline
|
||||
@loadeddata="onVideoDataLoad"
|
||||
@playing="onPlaying"
|
||||
@pause="onPaused"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue