Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85825338
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
36 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js
index 089cb1d267..482524eb66 100644
--- a/src/components/quick_filter_settings/quick_filter_settings.js
+++ b/src/components/quick_filter_settings/quick_filter_settings.js
@@ -1,139 +1,139 @@
import { mapState } from 'pinia'
import Popover from 'src/components/popover/popover.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons'
library.add(faFilter, faFont, faWrench)
const QuickFilterSettings = {
props: {
conversation: Boolean,
nested: Boolean,
},
components: {
Popover,
},
methods: {
setReplyVisibility(visibility) {
useSyncConfigStore().setSimplePrefAndSave({
path: 'replyVisibility',
value: visibility,
})
- useStatusesStore().queueFlushAll()
+ useStatusesStore().requireReloadAll()
},
openTab(tab) {
useInterfaceStore().openSettingsModalTab(tab)
},
},
computed: {
...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, {
mobileLayout: (state) => state.layoutType === 'mobile',
}),
triggerAttrs() {
if (this.mobileLayout) {
return {}
} else {
return {
title: this.$t('timeline.quick_filter_settings'),
}
}
},
mainClass() {
if (this.mobileLayout) {
return 'main-button'
} else {
return 'dropdown-item'
}
},
loggedIn() {
return !!useUsersStore().currentUser
},
replyVisibilitySelf: {
get() {
return this.mergedConfig.replyVisibility === 'self'
},
set() {
this.setReplyVisibility('self')
},
},
replyVisibilityFollowing: {
get() {
return this.mergedConfig.replyVisibility === 'following'
},
set() {
this.setReplyVisibility('following')
},
},
replyVisibilityAll: {
get() {
return this.mergedConfig.replyVisibility === 'all'
},
set() {
this.setReplyVisibility('all')
},
},
hideMedia: {
get() {
return (
this.mergedConfig.hideAttachments ||
this.mergedConfig.hideAttachmentsInConv
)
},
set(value) {
useLocalConfigStore().set({
path: 'hideAttachments',
value,
})
useLocalConfigStore().set({
path: 'hideAttachmentsInConv',
value,
})
},
},
hideMutedPosts: {
get() {
return this.mergedConfig.hideFilteredStatuses
},
set(value) {
useSyncConfigStore().setSimplePrefAndSave({
path: 'hideFilteredStatuses',
value,
})
},
},
muteBotStatuses: {
get() {
return this.mergedConfig.muteBotStatuses
},
set(value) {
useSyncConfigStore().setSimplePrefAndSave({
path: 'muteBotStatuses',
value,
})
},
},
muteSensitiveStatuses: {
get() {
return this.mergedConfig.muteSensitiveStatuses
},
set(value) {
useSyncConfigStore().setSimplePrefAndSave({
path: 'muteSensitiveStatuses',
value,
})
},
},
},
}
export default QuickFilterSettings
diff --git a/src/components/settings_modal/tabs/clutter_tab.js b/src/components/settings_modal/tabs/clutter_tab.js
index a3f6e77a50..18300231c1 100644
--- a/src/components/settings_modal/tabs/clutter_tab.js
+++ b/src/components/settings_modal/tabs/clutter_tab.js
@@ -1,44 +1,44 @@
import { mapState } from 'pinia'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import HelpIndicator from '../helpers/help_indicator.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useStatusesStore } from 'src/stores/statuses.js'
const ClutterTab = {
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting,
Checkbox,
Select,
HelpIndicator,
},
computed: {
...SharedComputedObject(),
...mapState(useInstanceCapabilitiesStore, ['shoutAvailable']),
...mapState(useInstanceStore, {
showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
instanceSpecificPanelPresent: (store) =>
store.instanceIdentity.showInstanceSpecificPanel &&
store.instanceIdentity.instanceSpecificPanelContent,
}),
},
// Updating nested properties
watch: {
replyVisibility() {
- useStatusesStore().queueFlushAll()
+ useStatusesStore().requireReloadAll()
},
},
}
export default ClutterTab
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 9d49eca81d..fbc7b89027 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -1,279 +1,279 @@
import { cloneDeep } from 'lodash'
import { mapActions, mapState } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import HelpIndicator from '../helpers/help_indicator.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import {
newExporter,
newImporter,
} from 'src/services/export_import/export_import.js'
const SUPPORTED_TYPES = new Set(['word', 'regexp', 'user', 'user_regexp'])
const FilteringTab = {
data() {
return {
replyVisibilityOptions: ['all', 'following', 'self'].map((mode) => ({
key: mode,
value: mode,
label: this.$t(`settings.reply_visibility_${mode}`),
})),
muteBlockLv1Options: ['ask', 'forever', 'temporarily'].map((mode) => ({
key: mode,
value: mode,
label: this.$t(`user_card.mute_block_${mode}`),
})),
muteFiltersDraftObject: cloneDeep(
useSyncConfigStore().prefsStorage.simple.muteFilters,
),
muteFiltersDraftDirty: Object.fromEntries(
Object.entries(
useSyncConfigStore().prefsStorage.simple.muteFilters,
).map(([k]) => [k, false]),
),
exportedFilter: null,
filterImporter: newImporter({
validator(parsed) {
if (Array.isArray(parsed)) return false
if (!SUPPORTED_TYPES.has(parsed.type)) return false
return true
},
onImport: (data) => {
const {
enabled = true,
expires = null,
hide = false,
name = '',
value = '',
caseSensitive = false,
} = data
this.createFilter({
enabled,
expires,
hide,
name,
value,
caseSensitive,
})
},
onImportFailure(result) {
console.error('Failure importing filter:', result)
useInterfaceStore().pushGlobalNotice({
messageKey: 'settings.filter.import_failure',
level: 'error',
})
},
}),
filterExporter: newExporter({
filename: 'pleromafe_mute-filter',
getExportedObject: () => this.exportedFilter,
}),
}
},
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting,
Checkbox,
Select,
HelpIndicator,
},
computed: {
...SharedComputedObject(),
...mapState(useSyncConfigStore, {
muteFilters: (store) =>
Object.entries(store.prefsStorage.simple.muteFilters),
muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters,
}),
...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
onMuteDefaultActionLv1: {
get() {
const value = useMergedConfigStore().mergedConfig.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set(value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.setPreference({
path: 'simple.onMuteDefaultAction',
value: realValue,
})
},
},
onBlockDefaultActionLv1: {
get() {
const value = useMergedConfigStore().mergedConfig.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set(value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.setPreference({
path: 'simple.onBlockDefaultAction',
value: realValue,
})
},
},
muteFiltersDraft() {
return Object.entries(this.muteFiltersDraftObject)
},
muteFiltersExpired() {
const now = Date.now()
return Object.entries(this.muteFiltersDraftObject).filter(
([, { expires }]) => expires != null && expires <= now,
)
},
},
methods: {
...mapActions(useSyncConfigStore, [
'setPreference',
'setSimplePrefAndSave',
'unsetSimplePrefAndSave',
'unsetPreference',
'unsetPrefAndSave',
'pushSyncConfig',
]),
getDatetimeLocal(timestamp) {
const date = new Date(timestamp)
const fmt = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 2 })
const datetime = [
date.getFullYear(),
'-',
fmt.format(date.getMonth() + 1),
'-',
fmt.format(date.getDate()),
'T',
fmt.format(date.getHours()),
':',
fmt.format(date.getMinutes()),
].join('')
return datetime
},
checkRegexValid(id) {
const filter = this.muteFiltersObject[id]
if (filter.type !== 'regexp') return true
if (filter.type !== 'user_regexp') return true
const { value } = filter
let valid = true
try {
new RegExp(value)
} catch {
valid = false
console.error('Invalid RegExp: ' + value)
}
return valid
},
createFilter(filter) {
const newId = uuidv4()
const newFilter = {
type: 'word',
value: '',
name: 'New Filter',
enabled: true,
expires: null,
hide: false,
...filter,
}
newFilter.order = this.muteFilters.length + 2
this.muteFiltersDraftObject[newId] = newFilter
this.setSimplePrefAndSave({
path: 'muteFilters.' + newId,
value: newFilter,
})
},
exportFilter(id) {
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
delete this.exportedFilter.order
this.filterExporter.exportData()
},
importFilter() {
this.filterImporter.importData()
},
copyFilter(id) {
const filter = { ...this.muteFiltersDraftObject[id] }
const newId = uuidv4()
this.muteFiltersDraftObject[newId] = filter
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
},
deleteFilter(id) {
delete this.muteFiltersDraftObject[id]
this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
},
purgeExpiredFilters() {
this.muteFiltersExpired.forEach(([id]) => {
delete this.muteFiltersDraftObject[id]
this.unsetPreference({ path: 'simple.muteFilters.' + id, value: null })
})
this.pushSyncConfig()
},
updateFilter(id, field, value) {
const filter = { ...this.muteFiltersDraftObject[id] }
if (field === 'expires-never') {
if (!value) {
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
const date = Date.now() + offset
filter.expires = date
} else {
filter.expires = null
}
} else if (field === 'expires') {
const parsed = Date.parse(value)
filter.expires = parsed.valueOf()
} else {
filter[field] = value
}
this.muteFiltersDraftObject[id] = filter
this.muteFiltersDraftDirty[id] = true
},
saveFilter(id) {
this.setSimplePrefAndSave({
path: 'muteFilters.' + id,
value: this.muteFiltersDraftObject[id],
})
this.muteFiltersDraftDirty[id] = false
},
},
// Updating nested properties
watch: {
replyVisibility() {
- useStatusesStore().queueFlushAll()
+ useStatusesStore().requireReloadAll()
},
muteFiltersObject() {
this.muteFiltersDraftObject = cloneDeep(
useMergedConfigStore().mergedConfig.muteFilters,
)
},
},
}
export default FilteringTab
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index a76438ad0f..19b34b21e6 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,287 +1,286 @@
import { debounce, throttle } from 'lodash'
import { mapState } from 'pinia'
import Conversation from 'src/components/conversation/conversation.vue'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
import ScrollTopButton from 'src/components/scroll_top_button/scroll_top_button.vue'
import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faArrowUp,
faCheck,
faCircleNotch,
faCirclePlus,
faCog,
faMinus,
} from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = {
props: {
timelineRef: Object,
count: Number,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
inProfile: Boolean,
skipPinned: Boolean,
},
data() {
return {
showScrollTop: false,
paused: false,
unfocused: false,
virtualScrollIndex: 0,
blockingClicks: false,
}
},
components: {
ScrollTopButton,
Conversation,
TimelineMenu,
QuickFilterSettings,
QuickViewSettings,
},
computed: {
timeline() {
return useTimelinesStore()[this.timelineRef.name]
},
filteredVisibleStatuses() {
return this.timeline.order
.filter((id) => this.timeline.visibleStatusIds.has(id))
.map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (this.skipPinned ? !pinned : true))
},
newStatusCount() {
return this.timeline.newStatusCount
},
showLoadButton() {
- return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
+ return this.timeline.newStatusCount > 0 || this.timeline.reloadNeeded
},
loadButtonString() {
- if (this.timeline.flushMarker !== 0) {
+ if (this.timeline.reloadNeeded) {
return this.$t('timeline.reload')
} else {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
}
},
mobileLoadButtonString() {
- if (this.timeline.flushMarker !== 0) {
+ if (this.timeline.reloadNeeded) {
return '+'
} else {
return this.newStatusCount > 99 ? '∞' : this.newStatusCount
}
},
classes() {
let rootClasses = !this.embedded
? ['panel', 'panel-default']
: ['-embedded']
if (this.blockingClicks)
rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(
!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body'],
),
body: ['timeline-body'].concat(
!this.embedded ? ['panel-body'] : ['panel-body'],
),
footer: ['timeline-footer'].concat(
!this.embedded ? ['panel-footer'] : ['panel-body'],
),
}
},
statusesToDisplay() {
const amount = this.timeline.visibleStatusIds.size
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 new Set(this.timeline.order.slice(min, max))
},
virtualScrollingEnabled() {
return useMergedConfigStore().mergedConfig.virtualScrolling
},
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
},
created() {
this.timelineChange(this.timelineRef)
},
mounted() {
if (document.hidden !== undefined) {
document.addEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
window.addEventListener('scroll', this.handleScroll)
setTimeout(this.determineVisibleStatuses, 250)
},
unmounted() {
this.timelineChange(null, this.timelineRef)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (document.hidden !== undefined)
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
},
methods: {
timelineChange(newTimeline, oldTimeline) {
const sameName = newTimeline?.name === oldTimeline?.name
const sameArgument = newTimeline?.argument === oldTimeline?.argument
if (sameName && sameArgument) return
if (oldTimeline) {
useTimelinesStore().deactivate(oldTimeline.name)
}
if (newTimeline) {
useTimelinesStore().activate(newTimeline.name, newTimeline.argument)
}
},
stopBlockingClicks: debounce(function () {
this.blockingClicks = false
}, 1000),
blockClicksTemporarily() {
if (!this.blockingClicks) {
this.blockingClicks = true
}
this.stopBlockingClicks()
},
handleShortKey(e) {
// Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses() {
- if (this.timeline.flushMarker !== 0) {
+ if (this.timeline.reloadNeeded) {
useTimelinesStore().clearTimeline(this.timelineRef.name)
- useTimelinesStore().queueFlush(this.timelineRef.name, '')
this.fetchOlderStatuses()
} else {
this.blockClicksTemporarily()
useTimelinesStore().showNewStatuses(this.timelineRef.name)
this.paused = false
}
window.scrollTo({ top: 0 })
},
fetchOlderStatuses: throttle(
function () {
this.timeline.fetcher.fetchOlder()
},
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.min(
Math.floor(statuses.length * (centerOfScreen / height)),
statuses.length - 1,
)
let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's
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() {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
!this.timeline.fetcher.loading.value &&
this.$el.offsetHeight > 0 &&
window.innerHeight + window.pageYOffset >= height - 750
) {
this.fetchOlderStatuses()
}
},
handleScroll: throttle(function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 200),
handleVisibilityChange() {
this.unfocused = document.hidden
},
},
watch: {
timelineRef(newTimeline, oldTimeline) {
this.timelineChange(newTimeline, oldTimeline)
},
filteredVisibleStatuses() {
this.determineVisibleStatuses()
},
newStatusCount(count) {
if (!useMergedConfigStore().mergedConfig.streaming) {
return
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
const doc = document.documentElement
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (
top < 15 &&
!this.paused &&
!(
this.unfocused &&
useMergedConfigStore().mergedConfig.pauseOnUnfocused
)
) {
this.showNewStatuses()
} else {
this.paused = true
}
}
},
},
}
export default Timeline
diff --git a/src/stores/fetchers/timeline_fetcher.js b/src/stores/fetchers/timeline_fetcher.js
index 25e6e895f4..bc8664dbd7 100644
--- a/src/stores/fetchers/timeline_fetcher.js
+++ b/src/stores/fetchers/timeline_fetcher.js
@@ -1,115 +1,115 @@
import { ref } from 'vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { ARGUMENT_MAP, useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js'
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
const REPLY_VISIBILITY_TIMELINES = new Set([
'friends',
'public',
'publicAndExternal',
'bubble',
])
const timelineFetcher = (timeline, argument, credentials) => {
const loading = ref(false)
const bottomedOut = ref(false)
const interval = ref(null)
const fetchAndUpdate = ({ older = false, showImmediately = false } = {}) => {
loading.value = true
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
const mainArg = ARGUMENT_MAP[timeline.name]
if (mainArg) args[mainArg] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = timeline.minId || null
} else {
args.sinceId = timeline.maxId || null
}
args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statusIds.size
return fetchTimeline(args)
.then(({ data: statuses, pagination, timestamp }) => {
if (!older && statuses.length >= 20 && numStatusesBeforeFetch > 0) {
- useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
+ useTimelinesStore().requireReload(timeline.name)
}
if (older && statuses.length === 0) {
bottomedOut.value = true
}
const processed = useStatusesStore()
.addNewStatuses({ statuses, timestamp })
.map(({ id }) => id)
useTimelinesStore().addStatusesToTimeline(timeline.name, argument, {
statuses: processed,
showImmediately,
older,
pagination,
})
return { statuses, pagination }
})
.catch((error) => {
if (error.statusCode === 403 && timeline === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return
}
console.error('Timeline Error', error)
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000,
})
})
.finally(() => {
loading.value = false
})
}
const startFetching = () => {
if (interval.value) throw new Error('Interval already exists!')
fetchAndUpdate({
showImmediately: timeline.visibleStatusIds.size === 0,
})
interval.value = promiseInterval(fetchAndUpdate, 10000)
}
const stopFetching = () => {
interval.value.stop()
interval.value = null
}
return {
startFetching,
stopFetching,
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
loading,
bottomedOut,
}
}
export default timelineFetcher
diff --git a/src/stores/timelines.js b/src/stores/timelines.js
index ded412d4bc..6840a28f5a 100644
--- a/src/stores/timelines.js
+++ b/src/stores/timelines.js
@@ -1,321 +1,321 @@
import { first, last } from 'lodash'
import { defineStore } from 'pinia'
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
const emptyTl = (name, argument = null) => {
const result = {
name,
order: [],
statusIds: new Set(),
visibleStatusIds: new Set(),
newStatusCount: 0,
maxId: '',
minId: '',
streaming: false,
- flushMarker: 0,
+ reloadNeeded: false,
fetcher: null,
socket: null,
}
const property = ARGUMENT_MAP[name]
if (property) {
result[property] = argument
}
if (name === 'dms' || name === 'friends') {
result.persistent = true
}
return result
}
export const ARGUMENT_MAP = {
tag: 'tag',
list: 'listId',
bookmarks: 'bookmarkFolderId',
quotes: 'statusId',
search: 'query',
user: 'userId',
userPinned: 'userId',
media: 'userId',
}
const TIMELINES = new Set([
'mentions',
'public',
'user',
'userPinned',
'media',
'favorites',
'publicAndExternal',
'friends',
'tag',
'dms',
'bookmarks',
'list',
'bubble',
'quotes',
'search',
])
export const defaultState = () => {
return Object.fromEntries([...TIMELINES].map((name) => [name, emptyTl(name)]))
}
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
export const useTimelinesStore = defineStore('timelines', {
state: defaultState,
actions: {
// (De)Initialization stuff
activate(timelineName, argument, persistent) {
const timeline = this[timelineName]
if (timeline.persistent && !persistent) return
if (
timelineName === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
) {
console.warn("Instance doesn't support public favorites timeline")
return
}
const property = ARGUMENT_MAP[timelineName]
if (property) {
timeline[property] = argument
}
timeline.fetcher = timelineFetcher(
timeline,
argument,
useOAuthStore().token,
)
this.startFetchingTimeline(timelineName, argument, 'Timeline activated')
const streamName = TIMELINE_STREAM_MAP[timelineName]
if (streamName) {
const et = new EventTarget()
const openHandler = () => this.onStreamConnect(timelineName, argument)
const closeHandler = () =>
this.onStreamDisconnect(timelineName, argument)
const messageHandler =
() =>
({ detail: message }) =>
this.onStreamMessage(timelineName, argument, message)
et.addEventListener('open', openHandler)
et.addEventListener('close', closeHandler)
et.addEventListener('update', messageHandler)
timeline.socket = {
stream: {
name: streamName,
argument,
},
et,
handlers: {
openHandler,
closeHandler,
messageHandler,
},
}
useStreamingStore().addSubscriber(timeline.socket)
}
},
deactivate(timelineName, persistent) {
const timeline = this[timelineName]
if (timeline.persistent && !persistent) return
if (!timeline.streaming) {
this.stopFetchingTimeline(timelineName, 'Timeline deactivation')
}
if (timeline.socket) {
useStreamingStore().removeSubscriber(timeline.socket)
const { openHandler, closeHandler, messageHandler } =
timeline.socket.handlers
timeline.socket.et.removeEventListener('open', openHandler)
timeline.socket.et.removeEventListener('close', closeHandler)
timeline.socket.et.removeEventListener('message', messageHandler)
}
this[timelineName] = emptyTl(timelineName)
},
clearTimeline(timelineName) {
const timeline = this[timelineName]
timeline.order = []
timeline.statusIds = new Set()
timeline.visibleStatusIds = new Set()
timeline.newStatusCount = 0
timeline.maxId = ''
timeline.minId = ''
- timeline.flushMarker = 0
+ timeline.reloadNeeded = false
},
activatePersistents() {
TIMELINES.forEach((name) => {
if (this[name].persistent) {
this.activate(name, undefined, true)
}
})
},
deactivateAll() {
TIMELINES.forEach((name) => {
try {
this.deactivate(name, true)
} catch (e) {
console.error(`Failed to deactivate timeline ${name}:`, e)
}
})
},
// Update stuff
addStatusesToTimeline(
timelineName,
argument,
{
statuses,
showImmediately = false,
noIdUpdate = false,
pagination = {},
older = false,
},
) {
if (statuses.length === 0) return
const timeline = this[timelineName]
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
// Same can happen with tags etc.
const property = ARGUMENT_MAP[timelineName]
if (property && timeline[property] !== argument) {
return
}
if (!noIdUpdate) {
this.updateTimelineExtremes(timeline, pagination)
}
const filtered = statuses.filter((id) => !timeline.statusIds.has(id))
if (older) {
timeline.order.push(...filtered)
} else {
timeline.order.unshift(...filtered)
}
statuses.forEach((statusId) => {
const isNew = !timeline.statusIds.has(statusId)
timeline.statusIds.add(statusId)
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusIds.add(statusId)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
})
},
onStreamMessage(timeline, argument, event) {
this.addStatusesToTimeline(timeline, argument, {
statuses: [event.data.status.id],
})
},
// Poll & Push
onStreamConnect(timeline) {
console.debug('[Timelines] Stream connected', timeline)
this[timeline].streaming = true
this.stopFetchingTimeline(timeline, 'Socket connected')
},
onStreamDisconnect(timeline, argument) {
console.debug('[Timelines] Stream disconnected', timeline, argument)
this[timeline].streaming = false
this.startFetchingTimeline(timeline, argument, 'Socket disconnected')
},
startFetchingTimeline(timelineName, argument, reason) {
console.debug(
'[Timelines] Starting fetching timeline',
timelineName,
argument,
'Reason:',
reason,
)
const timeline = this[timelineName]
timeline.fetcher.startFetching()
},
stopFetchingTimeline(timelineName, reason) {
console.debug(
'[Timelines] Stopped fetching timeline',
timelineName,
'Reason:',
reason,
)
const timeline = this[timelineName]
timeline.fetcher.stopFetching()
},
// Queues & Timeline manip
updateTimelineExtremes(timeline, pagination = {}) {
// Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? last(timeline.order) ?? ''
const maxNew = pagination.minId ?? first(timeline.order) ?? ''
const newer = maxNew > timeline.maxId
const older = minNew < timeline.minId
if (newer || timeline.maxId === '') {
timeline.maxId = maxNew
}
if (older || timeline.minId === '') {
timeline.minId = minNew
}
this.syncOrder(timeline)
},
showNewStatuses(timelineName) {
const timeline = this[timelineName]
timeline.newStatusCount = 0
timeline.visibleStatusIds = new Set([...timeline.statusIds])
},
syncOrder(timeline) {
timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id))
},
- queueFlush(timeline, id) {
- this[timeline].flushMarker = id
+ requireReload(timeline, id) {
+ this[timeline].reloadNeeded = true
},
- queueFlushAll() {
+ requireReloadAll() {
Object.keys(this).forEach((timeline) => {
- this[timeline].flushMarker = this[timeline].maxId
+ this[timeline].reloadNeeded = true
})
},
// Misc
wipeStatuses(ids) {
TIMELINES.forEach((timelineName) => {
const timeline = this[timelineName]
ids.forEach((id) => {
timeline.statusIds.delete(id)
timeline.visibleStatusIds.delete(id)
})
this.syncOrder(timeline)
})
},
},
})
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 12:10 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798395
Default Alt Text
(36 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline