Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85830624
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 79b8f493b0..1cfe1b3b45 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,233 +1,260 @@
import { debounce, throttle } from 'lodash-es'
-import { mapState } from 'pinia'
+import { storeToRefs } from 'pinia'
+import {
+ computed,
+ onMounted,
+ onUnmounted,
+ provide,
+ ref,
+ toRefs,
+ watch,
+} from 'vue'
+import { useI18n } from 'vue-i18n'
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,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
- inProfile: Boolean,
skipPinned: Boolean,
hideEmpty: Boolean,
},
- data() {
- return {
- showScrollTop: false,
- paused: false,
- unfocused: false,
- blockingClicks: false,
- }
- },
- provide() {
- return {
- profileUserId: this.inProfile && this.timelineRef.argument,
- }
- },
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))
+ setup(props, ctx) {
+ const { t } = useI18n()
+ const unfocused = ref(false)
+
+ // Timeline
+ const { timelineRef } = toRefs(props)
+ const timeline = computed(() => useTimelinesStore()[timelineRef.value.name])
+ const { skipPinned } = toRefs(props)
+ const filteredVisibleStatuses = computed(() => {
+ return timeline.value.order
+ .filter((id) => timeline.value.visibleStatusIds.has(id))
.map((id) => useStatusesStore().allStatuses.get(id))
- .filter(({ pinned }) => (this.skipPinned ? !pinned : true))
- },
- count() {
- return this.timeline.order.length
- },
- newStatusCount() {
- return this.timeline.newStatusCount
- },
- showLoadButton() {
- return this.timeline.newStatusCount > 0 || this.timeline.reloadNeeded
- },
- loadButtonString() {
- if (this.timeline.reloadNeeded) {
- return this.$t('timeline.reload')
- } else {
- return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
+ .filter(({ pinned }) => (skipPinned.value ? !pinned : true))
+ })
+
+ // Counter
+ const count = computed(() => timeline.value.order.length)
+ const newStatusCount = computed(() => timeline.value.newStatusCount)
+ const showLoadButton = computed(
+ () => timeline.value.newStatusCount > 0 || timeline.value.reloadNeeded,
+ )
+
+ // Showing new
+ const paused = ref(false)
+ watch(newStatusCount, (count) => {
+ if (!useMergedConfigStore().mergedConfig.streaming) {
+ return
}
- },
- mobileLoadButtonString() {
- if (this.timeline.reloadNeeded) {
- return '+'
+ if (count <= 0) return
+ // 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 &&
+ !paused.value &&
+ !(
+ unfocused.value &&
+ useMergedConfigStore().mergedConfig.pauseOnUnfocused
+ )
+ ) {
+ showNewStatuses()
} else {
- return this.newStatusCount > 99 ? '∞' : this.newStatusCount
+ paused.value = true
}
- },
- 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'],
- ),
+ })
+ const showNewStatuses = () => {
+ if (timeline.value.reloadNeeded) {
+ useTimelinesStore().clearTimeline(timelineRef.value.name)
+ fetchOlderStatuses()
+ } else {
+ blockClicksTemporarily()
+ useTimelinesStore().showNewStatuses(timelineRef.value.name)
+ paused.value = false
}
- },
- statusesToDisplay() {
- return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
- },
- ...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.scrollTo({ top: 0 })
}
- window.addEventListener('keydown', this.handleShortKey)
- window.addEventListener('scroll', this.handleScroll)
- },
- 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 fetchOlderStatuses = throttle(() => {
+ timeline.value.fetcher.fetchOlder()
+ }, 1000)
+
+ // Timeline change
+ const 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
+ }
+ watch(timelineRef, timelineChange, { immediate: true })
+ onUnmounted(() => {
+ timelineChange(null, timelineRef.value) // ????
+ })
+
+ // Misclick prevention
+ const blockingClicks = ref(false)
+ const stopBlockingClicks = debounce(() => {
+ blockingClicks.value = false
+ }, 1000)
+ const blockClicksTemporarily = () => {
+ if (!blockingClicks.value) {
+ blockingClicks.value = true
}
- this.stopBlockingClicks()
- },
- handleShortKey(e) {
+ stopBlockingClicks()
+ }
+
+ // Shortcuts
+ const 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.reloadNeeded) {
- useTimelinesStore().clearTimeline(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,
- ),
- scrollLoad() {
+ if (e.key === '.') showNewStatuses()
+ }
+ onMounted(() => {
+ window.addEventListener('keydown', handleShortKey)
+ })
+ onUnmounted(() => {
+ window.removeEventListener('keydown', handleShortKey)
+ })
+
+ // Scroll
+ const scrollLoad = () => {
// TODO simplify this logic
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
- !this.timeline.fetcher.loadingOlder &&
+ !timeline.value.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750
) {
- this.fetchOlderStatuses()
+ fetchOlderStatuses()
}
- },
- handleScroll: throttle(function (e) {
- this.scrollLoad(e)
- }, 200),
- handleVisibilityChange() {
- this.unfocused = document.hidden
- },
- },
- watch: {
- timelineRef(newTimeline, oldTimeline) {
- this.timelineChange(newTimeline, oldTimeline)
- },
- newStatusCount(count) {
- if (!useMergedConfigStore().mergedConfig.streaming) {
- return
+ }
+ const handleScroll = throttle((e) => {
+ scrollLoad(e)
+ }, 200)
+ onMounted(() => {
+ window.addEventListener('scroll', handleScroll)
+ })
+ onUnmounted(() => {
+ window.removeEventListener('scroll', handleScroll)
+ })
+
+ // Focused state
+ const handleVisibilityChange = () => {
+ unfocused.value = document.hidden
+ }
+ onMounted(() => {
+ if (document.hidden === undefined) return
+ document.addEventListener(
+ 'visibilitychange',
+ handleVisibilityChange,
+ false,
+ )
+ unfocused.value = document.hidden
+ })
+ onUnmounted(() => {
+ if (document.hidden === undefined) return
+ document.removeEventListener(
+ 'visibilitychange',
+ handleVisibilityChange,
+ false,
+ )
+ })
+
+ // Misc UI things
+ const classes = computed(() => {
+ let rootClasses = !embedded.value
+ ? ['panel', 'panel-default']
+ : ['-embedded']
+ if (blockingClicks.value)
+ rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
+ return {
+ root: rootClasses,
+ header: ['timeline-heading'].concat(
+ !embedded.value ? ['panel-heading', '-sticky'] : ['panel-body'],
+ ),
+ body: ['timeline-body'].concat(
+ !embedded.value ? ['panel-body'] : ['panel-body'],
+ ),
+ footer: ['timeline-footer'].concat(
+ !embedded.value ? ['panel-footer'] : ['panel-body'],
+ ),
}
- 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
- }
+ })
+ const loadButtonString = computed(() => {
+ if (timeline.value.reloadNeeded) {
+ return t('timeline.reload')
+ } else {
+ return `${t('timeline.show_new')} (${newStatusCount.value})`
+ }
+ })
+ const mobileLoadButtonString = computed(() => {
+ if (timeline.value.reloadNeeded) {
+ return '+'
+ } else {
+ return newStatusCount.value > 99 ? '∞' : newStatusCount.value
}
- },
+ })
+
+ const { layoutType } = storeToRefs(useInterfaceStore())
+ const mobileLayout = computed(() => layoutType.value === 'mobile')
+ const { footerSlipgate, embedded, hideEmpty } = toRefs(props)
+
+ return {
+ timelineRef,
+ timeline,
+ filteredVisibleStatuses,
+
+ count,
+ showLoadButton,
+
+ showNewStatuses,
+ fetchOlderStatuses,
+
+ classes,
+ loadButtonString,
+ mobileLoadButtonString,
+ mobileLayout,
+ footerSlipgate,
+ embedded,
+ hideEmpty,
+ }
},
}
export default Timeline
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 14bc77480e..065c581392 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,176 +1,181 @@
import { get } from 'lodash-es'
import FollowCard from 'src/components/follow_card/follow_card.vue'
import List from 'src/components/list/list.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Timeline from 'src/components/timeline/timeline.vue'
import UserCard from 'src/components/user_card/user_card.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 { useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch)
const defaultTabKey = 'statuses'
const UserProfile = {
data() {
return {
error: false,
userId: null,
tab: defaultTabKey,
footerRef: null,
}
},
created() {
const routeParams = this.$route.params
this.load({ name: routeParams.name, id: routeParams.id })
this.tab = get(this.$route, 'query.tab', defaultTabKey)
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},
+ provide() {
+ return {
+ profileUserId: this.userId,
+ }
+ },
updated() {
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},
unmounted() {
useInterfaceStore().setForeignProfileBackground(null)
useUsersStore().clearFollowLists(this.userId)
},
computed: {
favorites() {
return useTimelinesStore().favorites
},
media() {
return useTimelinesStore().media
},
isUs() {
return this.userId && this.userId === useUsersStore().currentUser?.id
},
user() {
return useUsersStore().findUser(this.userId)
},
isExternal() {
return this.$route.name === 'external-user-profile'
},
followsTabVisible() {
return this.isUs || !this.user.hide_follows
},
followersTabVisible() {
return this.isUs || !this.user.hide_followers
},
favoritesTabVisible() {
return (
this.isUs ||
(useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable &&
!this.user.hide_favorites)
)
},
compactProfiles() {
return useMergedConfigStore().mergedConfig.compactProfiles
},
friends() {
return [
...useUsersStore().relationshipsLists.friends.get(this.user).keys(),
].map((id) => useUsersStore().findUser(id))
},
followers() {
return [
...useUsersStore().relationshipsLists.followers.get(this.user).keys(),
].map((id) => useUsersStore().findUser(id))
},
},
methods: {
setFooterRef(el) {
this.footerRef = el
},
fetchUsers(group) {
return () =>
useUsersStore()
['fetch' + group](this.userId)
.then((result) => ({ items: result }))
},
load(userNameOrId) {
const loadById = (userId) => {
this.userId = userId
}
// Reset view
this.userId = null
this.error = false
const maybeId = userNameOrId.id
const maybeName = userNameOrId.name
// Check if user data is already loaded in store
const user = maybeId
? useUsersStore().findUser(maybeId)
: useUsersStore().findUserByName(maybeName)
if (user) {
loadById(user.id)
} else {
const promise = maybeId
? useUsersStore().fetchUser(maybeId)
: useUsersStore().fetchUserByName(maybeName)
promise
.then(({ id }) => loadById(id))
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') {
// Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
}
},
switchUser(userNameOrId) {
this.load(userNameOrId)
},
onTabSwitch(tab) {
this.tab = tab
this.$router.replace({ query: { tab } })
},
linkClicked({ target }) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
},
watch: {
'$route.params.id': function (newVal) {
if (newVal) {
this.switchUser({ id: newVal })
}
},
'$route.params.name': function (newVal) {
if (newVal) {
this.switchUser({ name: newVal })
}
},
'$route.query': function (newVal) {
this.tab = newVal.tab || defaultTabKey
},
},
components: {
UserCard,
Timeline,
List,
FollowCard,
TabSwitcher,
},
}
export default UserProfile
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 5:40 AM (15 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802584
Default Alt Text
(17 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline