Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None
diff --git a/src/api/timelines.js b/src/api/timelines.js
index 0679b1eecd..568b97a4fa 100644
--- a/src/api/timelines.js
+++ b/src/api/timelines.js
@@ -1,198 +1,204 @@
import { paramsString, promisedRequest } from './helpers.js'
import {
parseLinkHeaderPagination,
parseNotification,
parseStatus,
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
const MASTODON_USER_HOME_TIMELINE_URL = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
}) =>
`/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const MASTODON_LIST_TIMELINE_URL = (
id,
{ minId, sinceId, maxId, limit, replyVisibility },
) =>
`/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
}) =>
`/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const MASTODON_PUBLIC_TIMELINE = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
local,
remote,
onlyMedia,
}) =>
`/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}`
const MASTODON_TAG_TIMELINE_URL = (
tag,
{ minId, sinceId, maxId, limit, replyVisibility },
) =>
`/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
export const MASTODON_USER_TIMELINE_URL = (
id,
{ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia },
) =>
`/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}`
const MASTODON_USER_FAVORITES_TIMELINE_URL = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
}) =>
`/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const MASTODON_BOOKMARK_TIMELINE_URL = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
folderId,
}) =>
`/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}`
const PLEROMA_STATUS_QUOTES_URL = (
id,
{ minId, sinceId, maxId, limit, replyVisibility },
) =>
`/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const PLEROMA_USER_FAVORITES_TIMELINE_URL = (
id,
{ minId, sinceId, maxId, limit, replyVisibility },
) =>
`/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const AKKOMA_BUBBLE_TIMELINE_URL = ({
minId,
sinceId,
maxId,
limit,
replyVisibility,
}) =>
`/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
const MASTODON_USER_NOTIFICATIONS_URL = ({
minId,
sinceId,
maxId,
limit,
includeTypes,
replyVisibility,
}) =>
`/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}`
export const fetchTimeline = ({
timeline,
credentials,
sinceId,
minId,
maxId,
userId,
listId,
statusId,
tag,
withMuted,
replyVisibility = 'all',
includeTypes = [],
bookmarkFolderId,
}) => {
const timelineUrls = {
friends: MASTODON_USER_HOME_TIMELINE_URL,
public: MASTODON_PUBLIC_TIMELINE,
publicAndExternal: MASTODON_PUBLIC_TIMELINE,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
user: MASTODON_USER_TIMELINE_URL,
+ userPinned: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,
list: MASTODON_LIST_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL,
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
tag: MASTODON_TAG_TIMELINE_URL,
quotes: PLEROMA_STATUS_QUOTES_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
}
const urlFunc = timelineUrls[timeline]
const twoArgs = new Set([
'user',
+ 'userPinned',
'media',
'list',
'publicFavorites',
'tag',
'quotes',
])
const params = {
minId,
sinceId,
maxId,
limit: 20,
}
const id = (() => {
switch (timeline) {
case 'user':
+ case 'userPinned':
case 'media':
return userId
case 'list':
return listId
case 'quotes':
return statusId
case 'tag':
return tag
}
})()
const isNotifications = timeline === 'notifications'
if (timeline === 'media') {
params.onlyMedia = true
}
+ if (timeline === 'userPinned') {
+ params.pinned = true
+ }
if (timeline === 'public') {
params.local = true
}
if (timeline !== 'favorites' && timeline !== 'bookmarks') {
params.withMuted = withMuted
}
if (replyVisibility !== 'all') {
params.replyVisibility = replyVisibility
}
if (timeline === 'bookmarks' && bookmarkFolderId) {
params.folderId = bookmarkFolderId
}
if (isNotifications && includeTypes.length > 0) {
params.includeTypes = includeTypes
}
const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params)
return promisedRequest({ url, credentials }).then((result) => {
const pagination = parseLinkHeaderPagination(
result.response.headers.get('Link'),
{
flakeId: timeline !== 'bookmarks' && timeline !== 'notifications',
},
)
return {
...result,
data: result.data.map(isNotifications ? parseNotification : parseStatus),
pagination,
}
})
}
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 040ae80e7d..789e73985d 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,303 +1,292 @@
import { debounce, keyBy, 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 { useUsersStore } from 'src/stores/users.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,
- argument: String,
- embedded: Boolean,
count: Number,
- pinnedStatusIds: Set,
- inProfile: Boolean,
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,
bottomedOut: false,
virtualScrollIndex: 0,
blockingClicks: false,
}
},
components: {
ScrollTopButton,
Conversation,
TimelineMenu,
QuickFilterSettings,
QuickViewSettings,
},
computed: {
timeline() {
return useTimelinesStore()[this.timelineRef.name]
},
filteredVisibleStatuses() {
return [...this.timeline.visibleStatusesIds.keys()]
- .filter(
- (id) =>
- this.timelineRef.name !== 'user' ||
- (id >= this.timeline.minId && id <= this.timeline.maxId),
- )
.map((id) => this.timeline.statuses.get(id))
- },
- filteredPinnedStatusIds() {
- return (this.pinnedStatusIds || []).filter(
- (statusId) => this.timeline.statusesObject[statusId],
- )
+ .filter(({ pinned }) => this.skipPinned ? !pinned : true)
},
newStatusCount() {
return this.timeline.newStatusCount
},
showLoadButton() {
return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
},
loadButtonString() {
if (this.timeline.flushMarker !== 0) {
return this.$t('timeline.reload')
} else {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
}
},
mobileLoadButtonString() {
if (this.timeline.flushMarker !== 0) {
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'],
),
}
},
- // id map of statuses which need to be hidden in the main list due to pinning logic
- pinnedStatusIdsObject() {
- return keyBy(this.pinnedStatusIds)
- },
statusesToDisplay() {
const amount = this.timeline.visibleStatusesIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
- const nonPinnedIndex =
- this.virtualScrollIndex - this.filteredPinnedStatusIds.length
- const min = Math.max(0, nonPinnedIndex - statusesPerSide)
- const max = Math.min(amount, nonPinnedIndex + statusesPerSide)
+ const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
+ const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
return new Set(
[...this.timeline.visibleStatusesIds.keys()].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() {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (document.hidden !== undefined)
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
},
methods: {
timelineChange(newTimeline, oldTimeline) {
+ // TODO this might not be necessary if we optimize mergeOrAdd
+ const sameName = newTimeline?.name === oldTimeline?.name
+ const sameArgument = newTimeline?.argument === oldTimeline?.argument
+ if (sameName && sameArgument) return
+
if (oldTimeline && oldTimeline.name !== 'friends') {
useTimelinesStore().clearTimeline(oldTimeline.name)
}
useTimelinesStore().startFetchingTimeline(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) {
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
.fetchAndUpdate()
.then(({ statuses }) => {
if (statuses?.length === 0) {
this.bottomedOut = true
}
})
},
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() {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
this.timeline.loading === false &&
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/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 38a2ae1444..fec86c228e 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,143 +1,132 @@
<template>
<div :class="['Timeline', classes.root]">
<div
v-if="!embedded"
:class="classes.header"
>
<TimelineMenu
v-if="!embedded"
:timeline-name="timelineName"
/>
<ScrollTopButton />
<template v-if="mobileLayout">
<div
v-if="showLoadButton"
class="rightside-button"
>
<button
class="button-unstyled loadmore-button"
:title="loadButtonString"
@click.prevent="showNewStatuses"
>
<FAIcon
fixed-width
icon="circle-plus"
/>
<div class="badge -counter">
{{ mobileLoadButtonString }}
</div>
</button>
</div>
<div
v-else
class="loadmore-text faint veryfaint rightside-icon"
:title="$t('timeline.up_to_date')"
:aria-disabled="true"
@click.prevent
>
<FAIcon
fixed-width
icon="check"
/>
</div>
</template>
<template v-else>
<button
v-if="showLoadButton"
class="button-default loadmore-button"
@click.prevent="showNewStatuses"
>
{{ loadButtonString }}
</button>
<div
v-else
class="loadmore-text faint"
@click.prevent
>
{{ $t('timeline.up_to_date') }}
</div>
</template>
<QuickFilterSettings
v-if="!mobileLayout"
class="rightside-button"
/>
<QuickViewSettings
class="rightside-button"
/>
</div>
<div :class="classes.body">
<div
ref="timeline"
class="timeline"
role="feed"
>
- <Conversation
- v-for="statusId in filteredPinnedStatusIds"
- :key="statusId + '-pinned'"
- role="listitem"
- class="status-fadein"
- :status-id="statusId"
- :pinned-status-ids-object="pinnedStatusIdsObject"
- :in-profile="inProfile"
- :profile-user-id="userId"
- collapsable
- />
<Conversation
v-for="status in filteredVisibleStatuses"
:key="status.id"
role="listitem"
class="status-fadein"
:status-id="status.id"
:in-profile="inProfile"
:profile-user-id="userId"
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.has(status.id)"
collapsable
/>
</div>
</div>
<div :class="classes.footer">
<teleport
:to="footerSlipgate"
:disabled="!embedded || !footerSlipgate"
>
<div
v-if="count===0"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_statuses') }}
</div>
<div
v-else-if="bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_more_statuses') }}
</div>
<button
v-else-if="!timeline.loading"
class="button-unstyled -link"
@click.prevent="fetchOlderStatuses()"
>
<div class="new-status-notification text-center">
{{ $t('timeline.load_older') }}
</div>
</button>
<div
v-else
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
</teleport>
<!-- spacer to avoid having empty shrug -->
<span v-if="embedded && footerSlipgate" />
</div>
</div>
</template>
<script src="./timeline.js"></script>
<style src="./timeline.scss" lang="scss"> </style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 1b6b5f31c4..191b5c8eb2 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,180 +1,182 @@
import { get } from 'lodash'
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/statuses.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)
},
updated() {
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},
unmounted() {
- this.stopFetching()
useInterfaceStore().setForeignProfileBackground(null)
this.$store.dispatch('clearFollowers', this.userId)
this.$store.dispatch('clearFriends', this.userId)
},
computed: {
favorites() {
return useTimelinesStore().favorites
},
media() {
return useTimelinesStore().media
},
isUs() {
return (
this.userId &&
useUsersStore().currentUser.id &&
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 get(useUsersStore().findUser(this.userId), 'friendIds', []).map(
(id) => useUsersStore().findUser(id),
)
},
followers() {
return get(useUsersStore().findUser(this.userId), 'followerIds', []).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) => {
+ console.log('LOAD', 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 {
- ;(maybeId
- ? this.$store.dispatch('fetchUser', maybeId)
- : this.$store.dispatch('fetchUserByName', maybeName)
- )
+ 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) {
+ console.log('USER SWITCH')
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
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index e7381937d6..ca74de0d6c 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,128 +1,137 @@
<template>
<div>
<div
v-if="user"
class="user-profile panel panel-default"
>
<div class="panel-body card-wrapper">
<UserCard
:user-id="userId"
:switcher="true"
:compact="compactProfiles"
avatar-action="zoom"
:has-note-editor="true"
/>
</div>
<tab-switcher
+ v-if="userId"
:active-tab="tab"
:render-only-focused="true"
:on-switch="onTabSwitch"
>
<div
+ key="statuses"
class="statuses"
:label="$t('user_card.statuses')"
:count="user.statuses_count"
:title="$t('user_profile.timeline_title')"
>
+ <!--
<Timeline
key="statuses"
- :embedded="true"
- timeline-name="user"
- :argument="userId"
- :in-profile="true"
+ :timeline-ref="{ name: 'userPinned', argument: userId }"
+ embedded
+ in-profile
+ :footer-slipgate="footerRef"
+ />
+ -->
+ <Timeline
+ :timeline-ref="{ name: 'user', argument: userId }"
+ embedded
+ in-profile
+ skip-pinned
:footer-slipgate="footerRef"
/>
</div>
<div
v-if="followsTabVisible"
key="followees"
class="panel-body"
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
<List
:fetch-function="fetchUsers('Friends')"
:external-items="friends"
>
<template #item="{item}">
<FollowCard :user="item" />
</template>
</List>
</div>
<div
v-if="followersTabVisible"
key="followers"
class="panel-body"
:label="$t('user_card.followers')"
:disabled="!user.followers_count"
>
<List
:fetch-function="fetchUsers('Followers')"
:external-items="followers"
>
<template #item="{item}">
<FollowCard
:user="item"
:no-follows-you="isUs"
/>
</template>
</List>
</div>
<Timeline
key="media"
:label="$t('user_card.media')"
- :disabled="!media.visibleStatuses.length"
- :embedded="true"
+ :disabled="!media.visibleStatusesIds.size"
:title="$t('user_card.media')"
- timeline-name="media"
- :argument="userId"
- :in-profile="true"
+ :timeline-ref="{ name: 'media', argument: userId }"
+ embedded
+ in-profile
:footer-slipgate="footerRef"
/>
<Timeline
v-if="favoritesTabVisible"
key="favorites"
:label="$t('user_card.favorites')"
- :disabled="!favorites.visibleStatuses.length"
- :embedded="true"
+ :disabled="!favorites.visibleStatusesIds.size"
:title="$t('user_card.favorites')"
- timeline-name="favorites"
+ :timeline-ref="{ name: 'favorites', argument: userId }"
:argument="isUs ? undefined : userId"
- :in-profile="true"
+ embedded
+ in-profile
:footer-slipgate="footerRef"
/>
</tab-switcher>
<div
:ref="setFooterRef"
class="panel-footer"
/>
</div>
<div
v-else
class="panel user-profile-placeholder"
>
<div class="panel-heading">
<h1 class="title">
{{ $t('settings.profile_tab') }}
</h1>
</div>
<div class="panel-body">
<div
v-if="error"
class="alert error"
>
<span class="error-message">{{ error }}</span>
</div>
<FAIcon
v-else
spin
icon="circle-notch"
/>
</div>
</div>
</div>
</template>
<script src="./user_profile.js"></script>
<style src="./user_profile.scss" lang="scss"></style>
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 84c9805567..e056ece941 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -1,147 +1,146 @@
import { camelCase } from 'lodash'
import { promiseInterval } from '../promise_interval/promise_interval.js'
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'
const REPLY_VISIBILITY_TIMELINES = new Set([
'friends',
'public',
'publicAndExternal',
'bubble',
])
const fetchAndUpdate = ({
timeline,
argument,
credentials,
}, {
maxId,
sinceId,
older = false,
showImmediately = false,
}) => {
timeline.loading = true
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
args[ARGUMENT_MAP[timeline.name]] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = maxId || timeline.minId || null
} else {
if (sinceId === undefined) {
args.sinceId = timeline.maxId
} else if (sinceId !== null) {
args.sinceId = sinceId
}
}
args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statuses.size
return fetchTimeline(args)
.then((response) => {
const { data: statuses, pagination, timestamp } = response
if (
!older &&
statuses.length >= 20 &&
!timeline.loading &&
numStatusesBeforeFetch > 0
) {
useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
}
const processed = useStatusesStore()
.addNewStatuses({ statuses, timestamp })
.filter(Boolean)
console.log(timeline.name, argument, showImmediately, processed.length)
useTimelinesStore().addStatusesToTimeline(
timeline.name,
argument,
{
statuses,
showImmediately,
pagination,
}
)
return { statuses, pagination }
})
.catch((error) => {
if (error.statusCode === 403 && timeline === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return
}
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000,
})
})
.finally(() => {
timeline.loading = false
})
}
-const timelineFetcher = (timeline, argument, argumentKey, credentials) => {
+const timelineFetcher = (timeline, argument, credentials) => {
const state = {
interval: null
}
const boundFetchAndUpdate = ({
showImmediately,
maxId,
sinceId,
older,
} = {}) => fetchAndUpdate({
timeline,
argument,
- argumentKey,
credentials,
}, {
maxId,
sinceId,
older,
showImmediately,
})
const startFetching = () => {
if (state.interval) throw new Error('Interval already exists!')
boundFetchAndUpdate({
showImmediately: timeline.visibleStatusesIds.size === 0
})
state.interval = promiseInterval(boundFetchAndUpdate, 10000)
}
const stopFetching = () => {
state.interval.stop()
state.interval = null
}
return {
startFetching,
stopFetching,
fetchAndUpdate: boundFetchAndUpdate,
}
}
export default timelineFetcher
diff --git a/src/stores/timelines.js b/src/stores/timelines.js
index 5f78440fd2..3ba3cb60b9 100644
--- a/src/stores/timelines.js
+++ b/src/stores/timelines.js
@@ -1,233 +1,234 @@
import { first, last, max, min } from 'lodash'
import { defineStore } from 'pinia'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
const emptyTl = (name, argument = null) => {
const result = {
name,
statuses: new Map(),
visibleStatusesIds: new Set(),
newStatusCount: 0,
maxId: 0,
minId: 0,
minVisibleId: 0,
loading: false,
flushMarker: 0,
fetcher: null,
}
- const property = USER_TIMELINES.has(name) ? 'userId' : ARGUMENT_MAP[name]
+ const property = ARGUMENT_MAP[name]
if (property) {
result[property] = argument
}
return result
}
export const ARGUMENT_MAP = {
tag: 'tag',
list: 'listId',
bookmarks: 'bookmarkFolderId',
quotes: 'statusId',
search: 'query',
+ user: 'userId',
+ userPinned: 'userId',
+ media: 'userId',
}
export const defaultState = () => {
return Object.fromEntries([
'mentions',
'public',
'user',
'userPinned',
'media',
'favorites',
'publicAndExternal',
'friends',
'tag',
'dms',
'bookmarks',
'list',
'bubble',
'quotes',
'search',
].map((name) => [name, emptyTl(name)]))
}
-const USER_TIMELINES = new Set(['user', 'userPinned', 'media', 'favorites'])
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
export const useTimelinesStore = defineStore('timelines', {
state: defaultState,
actions: {
addStatusesToTimeline(
timelineName,
argument,
{
statuses,
showImmediately = false,
noIdUpdate = false,
pagination = {},
nested = 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 = USER_TIMELINES.has(name) ? 'userId' : ARGUMENT_MAP[name]
+ const property = ARGUMENT_MAP[name]
if (property && timeline[property] !== argument) {
return
}
if (!noIdUpdate) {
this.updateTimelineExtremes(
timeline,
statuses.map((x) => x.id),
pagination,
)
}
statuses.forEach((status) => {
const isNew = !timeline.statuses.has(status.id)
timeline.statuses.set(status.id, status)
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusesIds.add(status.id)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
if (nested) return
// We are mentioned in a post
if (
status.type === 'status' &&
status.attentions.some(
({ id }) => id === useUsersStore().currentUser?.id,
)
) {
// Add the mention to the mentions timeline
if (timeline !== this.mentions) {
this.addStatusesToTimeline('mentions', null, { statuses, nested: true })
}
}
if (status.visibility === 'direct') {
if (timeline !== this.dms) {
this.addStatusesToTimeline('dms', null, { statuses, nested: true })
}
}
})
},
// Fetchers
startFetchingTimeline(timelineName, argument) {
const timeline = this[timelineName]
if (timeline.fetcher) return
if (
timelineName === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
) {
return
}
timeline.fetcher = timelineFetcher(
timeline,
argument,
- ARGUMENT_MAP[timeline.name],
useOAuthStore().token,
)
timeline.fetcher.startFetching()
},
stopFetchingTimeline(timelineName) {
const timeline = this[timelineName]
timeline.fetcher?.stopFetching()
timeline.fetcher = null
},
// Queues & Timeline manip
updateTimelineExtremes(timeline, statuses, pagination = {}) {
// Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? min(...statuses) ?? ''
const maxNew = pagination.minId ?? max(...statuses) ?? ''
const newer = maxNew > timeline.maxId || timeline.maxId === ''
const older = minNew < timeline.minId || timeline.minId === ''
if (newer) {
timeline.maxId = maxNew
}
if (older) {
timeline.minId = minNew
}
},
resetStatuses() {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
this[key] = value
})
},
showNewStatuses(timelineName) {
const timeline = this[timelineName]
timeline.newStatusCount = 0
timeline.visibleStatusesIds = new Set(
[...timeline.statuses.keys()].slice(0, 50),
)
timeline.minVisibleId = last(timeline.visibleStatusesIds.keys())
timeline.minId = ''
timeline.maxId = ''
this.updateTimelineExtremes(timeline, [...timeline.statuses.keys()])
},
clearTimeline(timeline, excludeUserId = false) {
const userId = excludeUserId ? this[timeline].userId : undefined
this.stopFetchingTimeline(timeline)
this[timeline] = emptyTl(timeline, userId)
},
queueFlush(timeline, id) {
this[timeline].flushMarker = id
},
queueFlushAll() {
Object.keys(this).forEach((timeline) => {
this[timeline].flushMarker = this[timeline].maxId
})
},
// Misc
removeUserStatuses({ timelineName, userId }) {
const timeline = this.timelines[timelineName]
timeline.statuses
.values()
.filter(({ user }) => user.id === userId)
.forEach(({ id }) => {
timeline.statuses.delete(id)
timeline.visibleStatusesIds.delete(id)
})
timeline.minVisibleId =
timeline.visibleStatusesIds.size > 0
? last(timeline.visibleStatusesIds).id
: 0
timeline.maxId =
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
},
},
})

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 2:12 AM (15 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803505
Default Alt Text
(40 KB)

Event Timeline