Merge branch 'users-statuses-pinia' into shigusegubu-themes3
This commit is contained in:
commit
a6f19bb213
43 changed files with 208 additions and 131 deletions
1
changelog.d/avatar_mentions.change
Normal file
1
changelog.d/avatar_mentions.change
Normal file
|
|
@ -0,0 +1 @@
|
|||
If user avatars next to mentions are enabled it will show empty placeholder avatar next to label while user is being fetched, to avoid jumps
|
||||
1
changelog.d/follower-remove.fix
Normal file
1
changelog.d/follower-remove.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix follower remove API call
|
||||
|
|
@ -1 +1,3 @@
|
|||
Added an indicator next to instance's name showing WebSocket connection status (if enabled)
|
||||
Added an indicator next to instance's name showing WebSocket connection status (if enabled).
|
||||
Timeline no longer show "loading" indicator at the bottom when fetching newer posts.
|
||||
Added small indicator on top of timeline when new posts are being fetched
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { get, reduce } from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
|
|
@ -13,6 +12,7 @@ import { useInterfaceStore } from 'src/stores/interface.js'
|
|||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||
|
||||
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||
|
|
@ -93,6 +93,7 @@ const conversation = {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:virtualHeight'],
|
||||
data() {
|
||||
return {
|
||||
focused: null,
|
||||
|
|
@ -101,6 +102,7 @@ const conversation = {
|
|||
inlineDivePosition: null,
|
||||
loadStatusError: null,
|
||||
unsuspendibleIds: new Set(),
|
||||
virtualHeight: 120,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
|
@ -108,6 +110,9 @@ const conversation = {
|
|||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateVirtualHeight()
|
||||
},
|
||||
computed: {
|
||||
status() {
|
||||
return useStatusesStore().allStatuses.get(this.statusId)
|
||||
|
|
@ -360,8 +365,8 @@ const conversation = {
|
|||
return !!(this.expanded || this.isPage)
|
||||
},
|
||||
hiddenStyle() {
|
||||
const height = this.status?.virtualHeight || '120px'
|
||||
return this.virtualHidden ? { height } : {}
|
||||
if (this.isExpanded) return {}
|
||||
return { height: this.virtualHeight + 'px' }
|
||||
},
|
||||
threadDisplayStatus() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
|
|
@ -388,11 +393,11 @@ const conversation = {
|
|||
maybeFocused() {
|
||||
return this.isExpanded ? this.focused : null
|
||||
},
|
||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState(useStreamingStore, {
|
||||
mastoUserSocketStatus: (state) => state.state,
|
||||
}),
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
...mapState(useInterfaceStore, {
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
},
|
||||
|
|
@ -426,10 +431,7 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
virtualHidden() {
|
||||
useStatusesStore().setVirtualHeight({
|
||||
statusId: this.statusId,
|
||||
height: `${this.$el.clientHeight}px`,
|
||||
})
|
||||
this.updateVirtualHeight()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -618,6 +620,16 @@ const conversation = {
|
|||
this.$router.push({ name: 'conversation', params: { id: data.id } })
|
||||
}
|
||||
},
|
||||
updateVirtualHeight() {
|
||||
this.$nextTick(() => {
|
||||
this.virtualHeight = this.$refs.body.getBoundingClientRect().height
|
||||
this.$emit('update:virtualHeight', {
|
||||
id: this.status.id,
|
||||
height: this.virtualHeight,
|
||||
top: this.$el.clientTop,
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<div
|
||||
v-if="isPage && !status"
|
||||
class="conversation-body"
|
||||
ref="body"
|
||||
:class="{ 'panel-body': isExpanded }"
|
||||
>
|
||||
<p v-if="!loadStatusError">
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
<div
|
||||
v-else
|
||||
class="conversation-body"
|
||||
ref="body"
|
||||
:class="{ 'panel-body': isExpanded }"
|
||||
>
|
||||
<div
|
||||
|
|
@ -116,6 +118,7 @@
|
|||
@goto="setFocused"
|
||||
@dive="() => diveIntoStatus(status.id)"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
@height-change="updateVirtualHeight"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
|
|
@ -174,6 +177,7 @@
|
|||
@goto="setFocused"
|
||||
@dive="diveIntoStatus"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
@height-change="updateVirtualHeight"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -200,6 +204,7 @@
|
|||
@goto="setFocused"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
@height-change="updateVirtualHeight"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -135,9 +135,9 @@ export default {
|
|||
this.showConfirmLogout()
|
||||
}
|
||||
},
|
||||
doLogout() {
|
||||
async doLogout() {
|
||||
await useUsersStore().logout()
|
||||
this.$router.replace('/main/public')
|
||||
useUsersStore().logout()
|
||||
this.hideConfirmLogout()
|
||||
},
|
||||
onSearchBarToggled(hidden) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useUsersStore } from 'src/stores/users.js'
|
||||
|
||||
import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js'
|
||||
|
||||
|
|
@ -18,12 +18,10 @@ const LoginForm = {
|
|||
error: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapState({
|
||||
loggingIn: (state) => state.users.loggingIn,
|
||||
}),
|
||||
...mapPiniaState(useOAuthStore, ['clientId', 'clientSecret']),
|
||||
...mapPiniaState(useInstanceStore, ['server', 'registrationOpen']),
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
...mapState(useUsersStore, ['loggingIn']),
|
||||
...mapState(useOAuthStore, ['clientId', 'clientSecret']),
|
||||
...mapState(useInstanceStore, ['server', 'registrationOpen']),
|
||||
...mapState(useAuthFlowStore, {
|
||||
isTokenAuth: (store) => store.requiredToken,
|
||||
isPasswordAuth: (store) => !store.requiredToken,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const MentionLink = {
|
|||
},
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
required: false,
|
||||
type: String,
|
||||
},
|
||||
content: {
|
||||
|
|
@ -75,11 +75,11 @@ const MentionLink = {
|
|||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.url && useUsersStore().findUserByUrl(this.url)
|
||||
return this.url ? useUsersStore().findUserByUrl(this.url) : null
|
||||
},
|
||||
isYou() {
|
||||
// FIXME why user !== currentUser???
|
||||
return this.user?.id === this.currentUser.id
|
||||
if (!this.currentUser) return false
|
||||
return this.user === this.currentUser
|
||||
},
|
||||
userName() {
|
||||
return this.user && this.userNameFullUi.split('@')[0]
|
||||
|
|
|
|||
|
|
@ -8,15 +8,20 @@
|
|||
:href="url"
|
||||
class="original"
|
||||
target="_blank"
|
||||
v-html="content"
|
||||
/><!-- eslint-enable vue/no-v-html -->
|
||||
><!-- eslint-enable vue/no-v-html -->
|
||||
<UserAvatar
|
||||
v-if="shouldShowAvatar"
|
||||
class="mention-avatar"
|
||||
:user-id="null"
|
||||
/>
|
||||
<span v-html="content" />
|
||||
</a>
|
||||
<UserPopover
|
||||
v-else
|
||||
:user-id="user.id"
|
||||
:disabled="!shouldShowTooltip"
|
||||
>
|
||||
<span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@ const MobileNav = {
|
|||
}
|
||||
},
|
||||
doLogout() {
|
||||
this.$router.replace('/main/public')
|
||||
useUsersStore().logout()
|
||||
this.hideConfirmLogout()
|
||||
this.$router.replace('/main/public')
|
||||
},
|
||||
markNotificationsAsSeen() {
|
||||
useNotificationsStore().markNotificationsAsSeen()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { mapState as mapVuexState } from 'vuex'
|
||||
|
||||
import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
|
@ -111,29 +111,29 @@ const NavPanel = {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapPiniaState(useAnnouncementsStore, {
|
||||
...mapState(useAnnouncementsStore, {
|
||||
unreadAnnouncementCount: 'unreadAnnouncementCount',
|
||||
supportsAnnouncements: (store) => store.supportsAnnouncements,
|
||||
}),
|
||||
...mapPiniaState(useInstanceCapabilitiesStore, [
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'pleromaChatMessagesAvailable',
|
||||
'pleromaBookmarkFoldersAvailable',
|
||||
'localBubble',
|
||||
]),
|
||||
...mapPiniaState(useInstanceStore, ['federating']),
|
||||
...mapPiniaState(useInstanceStore, {
|
||||
...mapState(useInstanceStore, ['federating']),
|
||||
...mapState(useInstanceStore, {
|
||||
privateMode: (store) => store.private,
|
||||
}),
|
||||
...mapPiniaState(useSyncConfigStore, {
|
||||
...mapState(useSyncConfigStore, {
|
||||
collapsed: (store) => store.prefsStorage.simple.collapseNav,
|
||||
pinnedItems: (store) =>
|
||||
new Set(store.prefsStorage.collections.pinnedNavItems),
|
||||
}),
|
||||
...mapPiniaState(useUsersStore, ['currentUser']),
|
||||
...mapState({
|
||||
...mapState(useUsersStore, ['currentUser']),
|
||||
...mapVuexState({
|
||||
followRequestCount: (state) => state.api.followRequests.length,
|
||||
}),
|
||||
...mapPiniaState(useChatsStore, ['unreadChatsCount']),
|
||||
...mapState(useChatsStore, ['unreadChatsCount']),
|
||||
timelinesItems() {
|
||||
return filterNavigation(
|
||||
Object.entries({ ...TIMELINES })
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { mapState as mapVuexState } from 'vuex'
|
||||
|
||||
import {
|
||||
filterNavigation,
|
||||
|
|
@ -59,26 +59,26 @@ const NavPanel = {
|
|||
getters() {
|
||||
return this.$store.getters
|
||||
},
|
||||
...mapPiniaState(useListsStore, {
|
||||
...mapState(useListsStore, {
|
||||
lists: getListEntries,
|
||||
}),
|
||||
...mapPiniaState(useAnnouncementsStore, {
|
||||
...mapState(useAnnouncementsStore, {
|
||||
supportsAnnouncements: (store) => store.supportsAnnouncements,
|
||||
}),
|
||||
...mapPiniaState(useBookmarkFoldersStore, {
|
||||
...mapState(useBookmarkFoldersStore, {
|
||||
bookmarks: getBookmarkFolderEntries,
|
||||
}),
|
||||
...mapPiniaState(useSyncConfigStore, {
|
||||
...mapState(useSyncConfigStore, {
|
||||
pinnedItems: (store) =>
|
||||
new Set(store.prefsStorage.collections.pinnedNavItems),
|
||||
}),
|
||||
...mapPiniaState(useInstanceStore, ['privateMode', 'federating']),
|
||||
...mapPiniaState(useInstanceCapabilitiesStore, [
|
||||
...mapState(useInstanceStore, ['privateMode', 'federating']),
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'pleromaChatMessagesAvailable',
|
||||
'localBubble',
|
||||
]),
|
||||
...mapPiniaState(useUsersStore, ['currentUser']),
|
||||
...mapState({
|
||||
...mapState(useUsersStore, ['currentUser']),
|
||||
...mapVuexState({
|
||||
followRequestCount: (state) => state.api.followRequests.length,
|
||||
}),
|
||||
pinnedList() {
|
||||
|
|
|
|||
|
|
@ -118,9 +118,6 @@ const Notification = {
|
|||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
getUser(notification) {
|
||||
return this.$store.state.users.usersObject[notification.from_profile.id]
|
||||
},
|
||||
interacted() {
|
||||
this.$emit('interacted')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ const oac = {
|
|||
clientSecret,
|
||||
instance: useInstanceStore().server,
|
||||
code: this.code,
|
||||
}).then(({ data: result }) => {
|
||||
}).then(async ({ data: result }) => {
|
||||
oauthStore.setToken(result.access_token)
|
||||
|
||||
useUsersStore().loginUser(result.access_token)
|
||||
await useUsersStore().loginUser(result.access_token)
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const QuickFilterSettings = {
|
|||
path: 'replyVisibility',
|
||||
value: visibility,
|
||||
})
|
||||
useStatusesStore().queueFlushAll()
|
||||
useStatusesStore().requireReloadAll()
|
||||
},
|
||||
openTab(tab) {
|
||||
useInterfaceStore().openSettingsModalTab(tab)
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@
|
|||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
<div
|
||||
v-if="serverValidationErrors.length"
|
||||
v-if="signUpErrors.length"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="alert error">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// eslint-disable-next-line no-unused
|
||||
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'pinia'
|
||||
import { Fragment } from 'vue'
|
||||
|
||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
|
@ -60,7 +60,7 @@ export default {
|
|||
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
||||
}
|
||||
},
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
...mapState(useInterfaceStore, {
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const ClutterTab = {
|
|||
// Updating nested properties
|
||||
watch: {
|
||||
replyVisibility() {
|
||||
useStatusesStore().queueFlushAll()
|
||||
useStatusesStore().requireReloadAll()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ const FilteringTab = {
|
|||
// Updating nested properties
|
||||
watch: {
|
||||
replyVisibility() {
|
||||
useStatusesStore().queueFlushAll()
|
||||
useStatusesStore().requireReloadAll()
|
||||
},
|
||||
muteFiltersObject() {
|
||||
this.muteFiltersDraftObject = cloneDeep(
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ const Status = {
|
|||
isPreview: Boolean,
|
||||
noHeading: Boolean,
|
||||
inlineExpanded: Boolean,
|
||||
showPinned: Boolean,
|
||||
inProfile: Boolean,
|
||||
inConversation: Boolean,
|
||||
inQuote: Boolean,
|
||||
|
|
@ -118,7 +117,13 @@ const Status = {
|
|||
|
||||
threadDisplayStatus: String,
|
||||
},
|
||||
emits: ['goto', 'dive', 'toggleExpanded', 'suspendableStateChange'],
|
||||
emits: [
|
||||
'goto',
|
||||
'dive',
|
||||
'toggleExpanded',
|
||||
'suspendableStateChange',
|
||||
'heightChange',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
replying: false,
|
||||
|
|
@ -189,7 +194,7 @@ const Status = {
|
|||
)
|
||||
|
||||
// User referenced in post might not be yet present in store
|
||||
// since their data is not included in status data
|
||||
// since their data is not included in status data, just the id
|
||||
return user?.statusnet_profile_url
|
||||
}
|
||||
},
|
||||
|
|
@ -370,7 +375,7 @@ const Status = {
|
|||
},
|
||||
replyToName() {
|
||||
if (this.mainStatus.in_reply_to_screen_name) {
|
||||
return this.status.in_reply_to_screen_name
|
||||
return this.mainStatus.in_reply_to_screen_name
|
||||
} else {
|
||||
const user = useUsersStore().findUser(
|
||||
this.mainStatus.in_reply_to_user_id,
|
||||
|
|
@ -387,7 +392,7 @@ const Status = {
|
|||
return uniqBy(combinedUsers, 'id')
|
||||
},
|
||||
tags() {
|
||||
return this.status.tags
|
||||
return [...this.status.tags]
|
||||
.filter((tagObj) => Object.hasOwn(tagObj, 'name'))
|
||||
.map((tagObj) => tagObj.name)
|
||||
.join(' ')
|
||||
|
|
@ -539,6 +544,7 @@ const Status = {
|
|||
this.headTailLinks = headTailLinks
|
||||
},
|
||||
toggleThreadDisplay() {
|
||||
// FIXME
|
||||
this.controlledToggleThreadDisplay()
|
||||
},
|
||||
scrollIfFocused(focused) {
|
||||
|
|
@ -557,8 +563,22 @@ const Status = {
|
|||
}
|
||||
}
|
||||
},
|
||||
onTransitionEnd() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
status: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
},
|
||||
replying() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
focused: function (id) {
|
||||
this.scrollIfFocused(id)
|
||||
},
|
||||
|
|
@ -584,6 +604,7 @@ const Status = {
|
|||
},
|
||||
isSuspendable: function (suspend) {
|
||||
this.$emit('suspendableStateChange', { id: this.status.id, suspend })
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@
|
|||
|
||||
<span class="heading-right">
|
||||
<span
|
||||
v-if="showPinned"
|
||||
v-if="mainStatus.pinned"
|
||||
class="pin"
|
||||
>
|
||||
<FAIcon
|
||||
|
|
@ -454,7 +454,10 @@
|
|||
</StatusPopover>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<Transition
|
||||
@after-leave="onTransitionEnd"
|
||||
name="fade"
|
||||
>
|
||||
<div
|
||||
v-if="shouldDisplayFavsAndRepeats"
|
||||
class="favs-repeated-users"
|
||||
|
|
@ -502,7 +505,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Transition>
|
||||
|
||||
<EmojiReactions
|
||||
v-if="(mergedConfig.emojiReactionsOnTimeline || focused) && (!noHeading && !isPreview)"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||
import Status from 'src/components/status/status.vue'
|
||||
|
||||
import { useStatusHistoryStore } from 'src/stores/statusHistory.js'
|
||||
|
||||
const StatusHistoryModal = {
|
||||
components: {
|
||||
Modal,
|
||||
StatusContent,
|
||||
Status,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@
|
|||
v-if="historyCount > 0"
|
||||
class="history-body"
|
||||
>
|
||||
<StatusContent
|
||||
<Status
|
||||
v-for="status in history"
|
||||
:key="status.id"
|
||||
:status="status"
|
||||
:statusoid="status"
|
||||
:is-preview="true"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ export default {
|
|||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
}
|
||||
if (props.disabled) {
|
||||
classesTab.push('disabled')
|
||||
classesWrapper.push('disabled')
|
||||
}
|
||||
if (props.image) {
|
||||
return (
|
||||
<div class={classesWrapper.join(' ')}>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const ThreadTree = {
|
|||
totalReplyCount: Object,
|
||||
totalReplyDepth: Object,
|
||||
},
|
||||
emits: ['suspendableStateChange', 'goto', 'dive'],
|
||||
emits: ['suspendableStateChange', 'goto', 'dive', 'heightChange'],
|
||||
computed: {
|
||||
currentReplies() {
|
||||
return this.getReplies(this.statusId).map(({ id }) => id)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
@goto="$emit('goto', statusId)"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||
@height-change="e => $emit('heightChange', e)"
|
||||
/>
|
||||
<div
|
||||
v-if="currentReplies.length > 0 && threadShowing"
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
@goto="(e) => $emit('goto', e)"
|
||||
@dive="(e) => $emit('dive', e)"
|
||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||
@height-change="e => $emit('heightChange', e)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ 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,
|
||||
|
|
@ -59,21 +58,24 @@ const Timeline = {
|
|||
.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.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
|
||||
|
|
@ -99,6 +101,7 @@ const Timeline = {
|
|||
}
|
||||
},
|
||||
statusesToDisplay() {
|
||||
if (!this.virtualScrollingEnabled) return this.visibleStatusIds
|
||||
const amount = this.timeline.visibleStatusIds.size
|
||||
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
|
||||
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
|
||||
|
|
@ -167,9 +170,8 @@ const Timeline = {
|
|||
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()
|
||||
|
|
@ -190,13 +192,12 @@ const Timeline = {
|
|||
if (!this.virtualScrollingEnabled) return
|
||||
|
||||
const statuses = this.$refs.timeline.children
|
||||
if (statuses.length === 0) return
|
||||
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
|
||||
|
|
@ -234,11 +235,11 @@ const Timeline = {
|
|||
this.virtualScrollIndex = approxIndex
|
||||
},
|
||||
scrollLoad() {
|
||||
// TODO simplify this logic
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||
if (
|
||||
!this.timeline.fetcher.loading.value &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
!this.timeline.fetcher.loadingOlder.value &&
|
||||
window.innerHeight + window.pageYOffset >= height - 750
|
||||
) {
|
||||
this.fetchOlderStatuses()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div :class="['Timeline', classes.root]">
|
||||
<!-- there is a brief moment during logout when old timeline gets forcibly deactivated -->
|
||||
<div v-if="timeline.fetcher" :class="['Timeline', classes.root]">
|
||||
<div
|
||||
v-if="!embedded"
|
||||
:class="classes.header"
|
||||
|
|
@ -8,6 +9,16 @@
|
|||
v-if="!embedded"
|
||||
:timeline-name="timelineRef.name"
|
||||
/>
|
||||
<div
|
||||
v-if="timeline.fetcher.loadingNewer"
|
||||
class="loadingIndicator"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="circle-notch"
|
||||
spin
|
||||
/>
|
||||
</div>
|
||||
<ScrollTopButton />
|
||||
<template v-if="mobileLayout">
|
||||
<div
|
||||
|
|
@ -84,7 +95,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="classes.footer">
|
||||
<div v-if="!embedded || footerSlipgate" :class="classes.footer">
|
||||
<teleport
|
||||
:to="footerSlipgate"
|
||||
:disabled="!embedded || !footerSlipgate"
|
||||
|
|
@ -102,7 +113,7 @@
|
|||
{{ $t('timeline.no_more_statuses') }}
|
||||
</div>
|
||||
<button
|
||||
v-else-if="!timeline.fetcher.loading"
|
||||
v-else-if="!timeline.fetcher.loadingOlder"
|
||||
class="button-unstyled -link"
|
||||
@click.prevent="fetchOlderStatuses()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const UserAvatar = {
|
|||
props: {
|
||||
// UserID of a user to show avatar of
|
||||
userId: {
|
||||
required: true,
|
||||
required: false, // You can pass null to just render a placeholder
|
||||
type: String,
|
||||
},
|
||||
// Use less space and use alternative roundness
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
|
||||
&.-placeholder {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ const UserProfile = {
|
|||
},
|
||||
load(userNameOrId) {
|
||||
const loadById = (userId) => {
|
||||
console.log('LOAD', userId)
|
||||
this.userId = userId
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +138,6 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
switchUser(userNameOrId) {
|
||||
console.log('USER SWITCH')
|
||||
this.load(userNameOrId)
|
||||
},
|
||||
onTabSwitch(tab) {
|
||||
|
|
|
|||
|
|
@ -23,18 +23,14 @@
|
|||
key="statuses"
|
||||
class="statuses"
|
||||
:label="$t('user_card.statuses')"
|
||||
:count="user.statuses_count"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
>
|
||||
<!--
|
||||
<Timeline
|
||||
key="statuses"
|
||||
:timeline-ref="{ name: 'userPinned', argument: userId }"
|
||||
embedded
|
||||
in-profile
|
||||
:footer-slipgate="footerRef"
|
||||
/>
|
||||
-->
|
||||
<Timeline
|
||||
:timeline-ref="{ name: 'user', argument: userId }"
|
||||
embedded
|
||||
|
|
@ -81,7 +77,6 @@
|
|||
<Timeline
|
||||
key="media"
|
||||
:label="$t('user_card.media')"
|
||||
:disabled="media.visibleStatusIds.size === 0"
|
||||
:title="$t('user_card.media')"
|
||||
:timeline-ref="{ name: 'media', argument: userId }"
|
||||
embedded
|
||||
|
|
|
|||
|
|
@ -47,10 +47,12 @@ const UserProfileAdminView = {
|
|||
},
|
||||
methods: {
|
||||
fetchStatuses(page) {
|
||||
return useAdminSettingsStore().fetchStatuses({
|
||||
...this.fetchOptions,
|
||||
page,
|
||||
})
|
||||
return useAdminSettingsStore()
|
||||
.fetchStatuses({
|
||||
...this.fetchOptions,
|
||||
page,
|
||||
})
|
||||
.then(({ items }) => items)
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export const parseStatus = (data) => {
|
|||
output.raw_html = data.content
|
||||
output.emojis = data.emojis
|
||||
|
||||
output.tags = data.tags
|
||||
output.tags = new Set(data.tags ?? [])
|
||||
|
||||
output.edited_at = data.edited_at
|
||||
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ export const useAuthFlowStore = defineStore('authFlow', {
|
|||
this.settings = {}
|
||||
},
|
||||
async login({ access_token: accessToken }) {
|
||||
useOAuthStore().setToken(accessToken)
|
||||
useUsersStore().loginUser(accessToken, { root: true })
|
||||
await useOAuthStore().setToken(accessToken)
|
||||
await useUsersStore().loginUser(accessToken, { root: true })
|
||||
this.resetState()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,12 +18,17 @@ const REPLY_VISIBILITY_TIMELINES = new Set([
|
|||
])
|
||||
|
||||
const timelineFetcher = (timeline, argument, credentials) => {
|
||||
const loading = ref(false)
|
||||
const loadingNewer = ref(false)
|
||||
const loadingOlder = ref(false)
|
||||
const bottomedOut = ref(false)
|
||||
const interval = ref(null)
|
||||
|
||||
const fetchAndUpdate = ({ older = false, showImmediately = false } = {}) => {
|
||||
loading.value = true
|
||||
if (older) {
|
||||
loadingOlder.value = true
|
||||
} else {
|
||||
loadingNewer.value = true
|
||||
}
|
||||
|
||||
const { hideMutedPosts, replyVisibility } =
|
||||
useMergedConfigStore().mergedConfig
|
||||
|
|
@ -47,10 +52,11 @@ const timelineFetcher = (timeline, argument, credentials) => {
|
|||
|
||||
const numStatusesBeforeFetch = timeline.statusIds.size
|
||||
|
||||
if (bottomedOut.value) return
|
||||
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) {
|
||||
|
|
@ -84,7 +90,11 @@ const timelineFetcher = (timeline, argument, credentials) => {
|
|||
})
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
if (older) {
|
||||
loadingOlder.value = false
|
||||
} else {
|
||||
loadingNewer.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +117,8 @@ const timelineFetcher = (timeline, argument, credentials) => {
|
|||
startFetching,
|
||||
stopFetching,
|
||||
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
|
||||
loading,
|
||||
loadingOlder,
|
||||
loadingNewer,
|
||||
bottomedOut,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const useOAuthStore = defineStore('oauth', {
|
|||
this.userToken = token
|
||||
},
|
||||
clearToken() {
|
||||
this.userToken = false
|
||||
this.userToken = null
|
||||
},
|
||||
async createApp() {
|
||||
const instance = useInstanceStore().server
|
||||
|
|
|
|||
|
|
@ -540,10 +540,5 @@ export const useStatusesStore = defineStore('statuses', {
|
|||
})
|
||||
return removed
|
||||
},
|
||||
|
||||
// Misc
|
||||
setVirtualHeight({ statusId, height }) {
|
||||
this.allStatuses.get(statusId).virtualHeight = height
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -180,7 +180,6 @@ export const useStreamingStore = defineStore('streaming', {
|
|||
case 'delete':
|
||||
return [data.id]
|
||||
default:
|
||||
console.log('UNKNOWN', eventName, eventStream, data)
|
||||
return data
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const emptyTl = (name, argument = null) => {
|
|||
maxId: '',
|
||||
minId: '',
|
||||
streaming: false,
|
||||
flushMarker: 0,
|
||||
reloadNeeded: false,
|
||||
fetcher: null,
|
||||
socket: null,
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
timeline.newStatusCount = 0
|
||||
timeline.maxId = ''
|
||||
timeline.minId = ''
|
||||
timeline.flushMarker = 0
|
||||
timeline.reloadNeeded = false
|
||||
},
|
||||
activatePersistents() {
|
||||
TIMELINES.forEach((name) => {
|
||||
|
|
@ -258,14 +258,24 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
timeline.fetcher.startFetching()
|
||||
},
|
||||
stopFetchingTimeline(timelineName, reason) {
|
||||
console.debug(
|
||||
'[Timelines] Stopped fetching timeline',
|
||||
timelineName,
|
||||
'Reason:',
|
||||
reason,
|
||||
)
|
||||
const timeline = this[timelineName]
|
||||
timeline.fetcher.stopFetching()
|
||||
if (timeline.fetcher === null) {
|
||||
console.debug(
|
||||
'[Timelines] Already inactive timeline',
|
||||
timelineName,
|
||||
'Reason:',
|
||||
reason,
|
||||
)
|
||||
return
|
||||
} else {
|
||||
timeline.fetcher.stopFetching()
|
||||
console.debug(
|
||||
'[Timelines] Stopped fetching timeline',
|
||||
timelineName,
|
||||
'Reason:',
|
||||
reason,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Queues & Timeline manip
|
||||
|
|
@ -295,12 +305,12 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
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
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -354,11 +354,13 @@ describe('RichContent', () => {
|
|||
'<span class="MentionLink mention-link">',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'lol.tld/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'</span>',
|
||||
'</span>',
|
||||
|
|
@ -418,21 +420,25 @@ describe('RichContent', () => {
|
|||
'<span class="MentionLink mention-link">',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'lol.tld/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'</span>',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'lol.tld/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'</span>',
|
||||
'</span>',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ describe('Timelines store', () => {
|
|||
const sub = vi.fn()
|
||||
useStreamingStore().addSubscriber = sub
|
||||
|
||||
console.log(store.activate)
|
||||
store.activate('friends', undefined, true)
|
||||
|
||||
expect(sub).to.have.been.called
|
||||
|
|
@ -34,7 +33,6 @@ describe('Timelines store', () => {
|
|||
const sub = vi.fn()
|
||||
useStreamingStore().addSubscriber = sub
|
||||
|
||||
console.log(store.activate)
|
||||
store.activate('user', '1')
|
||||
|
||||
expect(sub).to.not.have.been.called
|
||||
|
|
|
|||
|
|
@ -1095,7 +1095,6 @@ describe('Users store', () => {
|
|||
const store = useUsersStore()
|
||||
const { storeAction, apiUrl } = actionKeys(action)
|
||||
await store[storeAction](userId)
|
||||
console.log(apiUrl)
|
||||
|
||||
expect(mockFetch).to.have.been.calledWith(
|
||||
USER_API[apiUrl](userId),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue