big cleanup, lint, relying on provide/inject instead of drilling props
This commit is contained in:
parent
74f8ef0719
commit
6ef8b36c73
10 changed files with 178 additions and 262 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { get, reduce } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, nextTick, ref, watch, toRefs } from 'vue'
|
import { computed, nextTick, provide, ref, toRefs, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
||||||
|
|
@ -16,7 +16,10 @@ import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||||
|
|
||||||
import { fetchConversation as apiFetchConversation, fetchStatus } from 'src/api/public.js'
|
import {
|
||||||
|
fetchConversation as apiFetchConversation,
|
||||||
|
fetchStatus,
|
||||||
|
} from 'src/api/public.js'
|
||||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
@ -36,24 +39,6 @@ library.add(
|
||||||
faTimes,
|
faTimes,
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
|
||||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
|
||||||
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
|
|
||||||
const seqA = Number(idA)
|
|
||||||
const seqB = Number(idB)
|
|
||||||
const isSeqA = !Number.isNaN(seqA)
|
|
||||||
const isSeqB = !Number.isNaN(seqB)
|
|
||||||
if (isSeqA && isSeqB) {
|
|
||||||
return seqA < seqB ? -1 : 1
|
|
||||||
} else if (isSeqA && !isSeqB) {
|
|
||||||
return -1
|
|
||||||
} else if (!isSeqA && isSeqB) {
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
return idA < idB ? -1 : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
statusId: {
|
statusId: {
|
||||||
|
|
@ -61,34 +46,12 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
collapsable: {
|
|
||||||
// Whether conversation can be collapsed
|
|
||||||
// i.e. when it's not a page
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isPage: {
|
isPage: {
|
||||||
// Whether conversation is rendered as a standalone page
|
// Whether conversation is rendered as a standalone page
|
||||||
// as opposed to embedded into a timeline
|
// as opposed to embedded into a timeline
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
pinnedStatusIdsObject: {
|
|
||||||
// Used for user profile, map of pinned statuses
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
inProfile: {
|
|
||||||
// Whether conversation is rendered in a user profile
|
|
||||||
// used for overriding muted status
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
profileUserId: {
|
|
||||||
// used with inProfile, user id of the profile
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
virtualHidden: {
|
virtualHidden: {
|
||||||
// Whether conversation is suspended. Controls rendering of statuses
|
// Whether conversation is suspended. Controls rendering of statuses
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
@ -106,25 +69,18 @@ export default {
|
||||||
},
|
},
|
||||||
setup(props, ctx) {
|
setup(props, ctx) {
|
||||||
const { emit } = ctx
|
const { emit } = ctx
|
||||||
const {
|
const { statusId } = toRefs(props)
|
||||||
statusId,
|
|
||||||
collapsable,
|
|
||||||
pinnedStatusIdsObject,
|
|
||||||
inProfile,
|
|
||||||
profileUserId,
|
|
||||||
virtualHidden,
|
|
||||||
} = toRefs(props)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const hoisted = {}
|
const router = useRouter()
|
||||||
|
|
||||||
// # Main Configuration
|
// # Main Configuration
|
||||||
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
||||||
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
|
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
|
||||||
const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
|
const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
|
||||||
const streamingEnabled = computed(() =>
|
const streamingEnabled = computed(
|
||||||
mergedConfig.value.useStreamingApi &&
|
() =>
|
||||||
mastoUserSocketStatus === WSConnectionStatus.JOINED
|
mergedConfig.value.useStreamingApi &&
|
||||||
|
mastoUserSocketStatus === WSConnectionStatus.JOINED,
|
||||||
)
|
)
|
||||||
|
|
||||||
// # Main things
|
// # Main things
|
||||||
|
|
@ -146,6 +102,24 @@ export default {
|
||||||
return statusId.value
|
return statusId.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sortById = (a, b) => {
|
||||||
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
|
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
|
||||||
|
const seqA = Number(idA)
|
||||||
|
const seqB = Number(idB)
|
||||||
|
const isSeqA = !Number.isNaN(seqA)
|
||||||
|
const isSeqB = !Number.isNaN(seqB)
|
||||||
|
if (isSeqA && isSeqB) {
|
||||||
|
return seqA < seqB ? -1 : 1
|
||||||
|
} else if (isSeqA && !isSeqB) {
|
||||||
|
return -1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return idA < idB ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
const conversationId = computed(() => getConversationId(statusId.value))
|
const conversationId = computed(() => getConversationId(statusId.value))
|
||||||
const conversation = computed(() => {
|
const conversation = computed(() => {
|
||||||
if (!status.value) {
|
if (!status.value) {
|
||||||
|
|
@ -167,11 +141,7 @@ export default {
|
||||||
})
|
})
|
||||||
const replies = computed(() =>
|
const replies = computed(() =>
|
||||||
conversation.value.reduce(
|
conversation.value.reduce(
|
||||||
(
|
(result, { id, in_reply_to_status_id: irid }, index) => {
|
||||||
result,
|
|
||||||
{ id, in_reply_to_status_id: irid },
|
|
||||||
index,
|
|
||||||
) => {
|
|
||||||
if (irid) {
|
if (irid) {
|
||||||
if (!result.has(irid)) {
|
if (!result.has(irid)) {
|
||||||
result.set(irid, new Set())
|
result.set(irid, new Set())
|
||||||
|
|
@ -184,18 +154,18 @@ export default {
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
new Map(),
|
new Map(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
const getReplies = (id) => replies.value.get(id) ?? new Set()
|
const getReplies = (id) => replies.value.get(id) ?? new Set()
|
||||||
|
|
||||||
|
provide('conversation', conversation)
|
||||||
|
provide('replies', replies)
|
||||||
|
|
||||||
const fetchConversation = async () => {
|
const fetchConversation = async () => {
|
||||||
if (status.value) {
|
if (status.value) {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: { ancestors, descendants },
|
||||||
ancestors,
|
timestamp,
|
||||||
descendants
|
|
||||||
},
|
|
||||||
timestamp
|
|
||||||
} = await apiFetchConversation({
|
} = await apiFetchConversation({
|
||||||
id: statusId.value,
|
id: statusId.value,
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
|
|
@ -225,11 +195,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const resetDisplayState = () => {
|
const resetDisplayState = () => {
|
||||||
hoisted.undive()
|
setFocused(statusId.value)
|
||||||
threadDisplay.value = new Map()
|
threadDisplay.value = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
// # Virtual scrolling stuff
|
// # Virtual scrolling stuff
|
||||||
|
const { virtualHidden } = toRefs(props)
|
||||||
const unsuspendibleIds = ref(new Set())
|
const unsuspendibleIds = ref(new Set())
|
||||||
const suspendable = computed(() => unsuspendibleIds.value.size === 0)
|
const suspendable = computed(() => unsuspendibleIds.value.size === 0)
|
||||||
const hide = computed(() => virtualHidden.value && suspendable.value)
|
const hide = computed(() => virtualHidden.value && suspendable.value)
|
||||||
|
|
@ -243,13 +214,13 @@ export default {
|
||||||
|
|
||||||
// # Misc UI things
|
// # Misc UI things
|
||||||
const loadStatusError = ref(null)
|
const loadStatusError = ref(null)
|
||||||
|
|
||||||
const { layoutType } = storeToRefs(useInterfaceStore())
|
const { layoutType } = storeToRefs(useInterfaceStore())
|
||||||
const mobileLayout = computed(() => layoutType.value === 'mobile')
|
const mobileLayout = computed(() => layoutType.value === 'mobile')
|
||||||
const firstStatus = computed(() => conversation.value[0])
|
const firstStatus = computed(() => conversation.value[0])
|
||||||
const lastStatus = computed(() => conversation.value[conversation.value.legnth - 1])
|
const lastStatus = computed(
|
||||||
|
() => conversation.value[conversation.value.legnth - 1],
|
||||||
|
)
|
||||||
const getStatusClasses = (status, active) => ({
|
const getStatusClasses = (status, active) => ({
|
||||||
'-virtual-active': active,
|
|
||||||
'-first': status.id === firstStatus.value?.id,
|
'-first': status.id === firstStatus.value?.id,
|
||||||
'-last': status.id === lastStatus.value?.id,
|
'-last': status.id === lastStatus.value?.id,
|
||||||
})
|
})
|
||||||
|
|
@ -268,13 +239,15 @@ export default {
|
||||||
resetDisplayState()
|
resetDisplayState()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
provide('isExpanded', isExpanded)
|
||||||
|
provide('isPage', isPage)
|
||||||
|
|
||||||
// # Focus
|
// # Focus
|
||||||
const focused = ref(null)
|
const focusedId = ref(statusId.value)
|
||||||
const maybeFocused = computed(() => isExpanded.value ? focused.value : null)
|
const focused = computed(() => (isExpanded.value ? focusedId.value : null))
|
||||||
const setFocused = (id) => {
|
const setFocused = (id) => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
focused.value = id
|
focusedId.value = id
|
||||||
|
|
||||||
if (!streamingEnabled.value) {
|
if (!streamingEnabled.value) {
|
||||||
useStatusesStore().fetchStatus(id)
|
useStatusesStore().fetchStatus(id)
|
||||||
|
|
@ -296,6 +269,7 @@ export default {
|
||||||
fetchConversation()
|
fetchConversation()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
provide('focused', focused)
|
||||||
|
|
||||||
// Component created
|
// Component created
|
||||||
if (isPage.value) {
|
if (isPage.value) {
|
||||||
|
|
@ -309,11 +283,15 @@ export default {
|
||||||
const isTreeView = computed(() => displayStyle.value === 'tree')
|
const isTreeView = computed(() => displayStyle.value === 'tree')
|
||||||
|
|
||||||
// ## Tree view settings
|
// ## Tree view settings
|
||||||
const treeViewIsSimple = computed(() => !mergedConfig.value.conversationTreeAdvanced)
|
const treeViewIsSimple = computed(
|
||||||
const shouldFadeAncestors = computed(() => mergedConfig.value.conversationTreeFadeAncestors)
|
() => !mergedConfig.value.conversationTreeAdvanced,
|
||||||
const otherRepliesButtonPosition = computed(() => mergedConfig.value.conversationOtherRepliesButton)
|
)
|
||||||
const showOtherRepliesButtonBelowStatus = computed(() => otherRepliesButtonPosition.value === 'below')
|
const shouldFadeAncestors = computed(
|
||||||
const showOtherRepliesButtonInsideStatus = computed(() => otherRepliesButtonPosition.value === 'inside')
|
() => mergedConfig.value.conversationTreeFadeAncestors,
|
||||||
|
)
|
||||||
|
const showOtherRepliesButtonBelowStatus = computed(
|
||||||
|
() => mergedConfig.value.conversationOtherRepliesButton === 'below',
|
||||||
|
)
|
||||||
const maxDepthToShowByDefault = computed(() => {
|
const maxDepthToShowByDefault = computed(() => {
|
||||||
// maxDepthInThread = max number of depths that is *visible*
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
// since our depth starts with 0 and "showing" means "showing children"
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
|
|
@ -323,19 +301,12 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
// ## Tree style state
|
// ## Tree style state
|
||||||
// ### Dive
|
|
||||||
const inlineDivePosition = ref(null)
|
|
||||||
const currentStatusId = computed(() => inlineDivePosition.value ?? statusId.value)
|
|
||||||
|
|
||||||
// ### Topology
|
// ### Topology
|
||||||
const ancestors = computed(() => {
|
const ancestors = computed(() => {
|
||||||
// First we fill map with empty sets and add given id's parent
|
// First we fill map with empty sets and add given id's parent
|
||||||
// as set's only element (if any)
|
// as set's only element (if any)
|
||||||
const parentMap = conversation.value.reduce(
|
const parentMap = conversation.value.reduce(
|
||||||
(
|
(result, { id, in_reply_to_status_id: irid }) => {
|
||||||
result,
|
|
||||||
{ id, in_reply_to_status_id: irid },
|
|
||||||
) => {
|
|
||||||
if (!result.has(id)) {
|
if (!result.has(id)) {
|
||||||
result.set(id, new Set())
|
result.set(id, new Set())
|
||||||
}
|
}
|
||||||
|
|
@ -345,7 +316,7 @@ export default {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
new Map()
|
new Map(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Next we iterate over each entry and fill the whole ancestry chain
|
// Next we iterate over each entry and fill the whole ancestry chain
|
||||||
|
|
@ -361,15 +332,19 @@ export default {
|
||||||
})
|
})
|
||||||
return parentMap
|
return parentMap
|
||||||
})
|
})
|
||||||
const getAncestorIds = (id) => ancestors.value.get(id) ?? new Set()
|
const topLevel = computed(() =>
|
||||||
const getAncestors = (id) => [...getAncestorIds(id)].map(getStatusObject).filter(Boolean)
|
[...ancestors.value.entries()]
|
||||||
const currentAncestors = computed(() => getAncestors(currentStatusId.value).reverse())
|
.filter(([id, ancestors]) => ancestors.size === 0)
|
||||||
const currentDepth = computed(() => currentAncestors.value.length)
|
.map(([id]) => getStatusObject(id)),
|
||||||
const topLevel = computed(() => [...ancestors.value.entries()]
|
|
||||||
.filter(([id, ancestors]) => ancestors.size === 0)
|
|
||||||
.map(([id]) => getStatusObject(id))
|
|
||||||
)
|
)
|
||||||
const currentStatus = computed(() => getStatusObject(currentStatusId.value))
|
const getAncestorIds = (id) => ancestors.value.get(id) ?? new Set()
|
||||||
|
const getAncestors = (id) =>
|
||||||
|
[...getAncestorIds(id)].map(getStatusObject).filter(Boolean)
|
||||||
|
const currentAncestors = computed(() =>
|
||||||
|
getAncestors(focusedId.value).reverse(),
|
||||||
|
)
|
||||||
|
const currentDepth = computed(() => currentAncestors.value.length)
|
||||||
|
const currentStatus = computed(() => getStatusObject(focusedId.value))
|
||||||
|
|
||||||
// ### Thread Display
|
// ### Thread Display
|
||||||
const threadDisplay = ref(new Map()) // id => 'showing' | 'hidden'
|
const threadDisplay = ref(new Map()) // id => 'showing' | 'hidden'
|
||||||
|
|
@ -390,6 +365,8 @@ export default {
|
||||||
return map
|
return map
|
||||||
}, new Map())
|
}, new Map())
|
||||||
})
|
})
|
||||||
|
provide('threadDisplay', threadDisplay)
|
||||||
|
provide('threadDisplayDefault', threadDisplayDefault)
|
||||||
|
|
||||||
const setThreadDisplayRecursively = (id, value) => {
|
const setThreadDisplayRecursively = (id, value) => {
|
||||||
threadDisplay.value.set(id, value)
|
threadDisplay.value.set(id, value)
|
||||||
|
|
@ -402,8 +379,12 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ## Derived values
|
// ## Derived values
|
||||||
const shouldShowAllConversationButton = computed(() => currentAncestors.value.length > 0 && topLevel.value.length > 1)
|
const shouldShowAllConversationButton = computed(
|
||||||
const shouldShowAncestors = computed(() => isExpanded.value && ancestors.value.get(currentStatusId.value) != null)
|
() => currentAncestors.value.length > 0 && topLevel.value.length > 1,
|
||||||
|
)
|
||||||
|
const shouldShowAncestors = computed(
|
||||||
|
() => isExpanded.value && ancestors.value.get(focusedId.value) != null,
|
||||||
|
)
|
||||||
|
|
||||||
// # Scrolling / diving
|
// # Scrolling / diving
|
||||||
const tryScrollTo = (id) => {
|
const tryScrollTo = (id) => {
|
||||||
|
|
@ -412,8 +393,6 @@ export default {
|
||||||
}
|
}
|
||||||
if (isPage.value) {
|
if (isPage.value) {
|
||||||
router.push({ name: 'conversation', params: { statusId: id } })
|
router.push({ name: 'conversation', params: { statusId: id } })
|
||||||
} else {
|
|
||||||
inlineDivePosition.value = id
|
|
||||||
}
|
}
|
||||||
// Because the conversation can be unmounted when out of sight
|
// Because the conversation can be unmounted when out of sight
|
||||||
// and mounted again when it comes into sight,
|
// and mounted again when it comes into sight,
|
||||||
|
|
@ -434,47 +413,32 @@ export default {
|
||||||
setFocused(id)
|
setFocused(id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const goToCurrent = () => {
|
|
||||||
tryScrollTo(diveRoot)
|
|
||||||
}
|
|
||||||
const diveIntoStatus = (id) => {
|
const diveIntoStatus = (id) => {
|
||||||
tryScrollTo(id)
|
tryScrollTo(id)
|
||||||
}
|
}
|
||||||
const diveToTopLevel = () => {
|
const diveToTopLevel = () => {
|
||||||
tryScrollTo(currentAncestors.value[0].id)
|
tryScrollTo(currentAncestors.value[0].id)
|
||||||
}
|
}
|
||||||
const undive = () => {
|
|
||||||
inlineDivePosition.value = null
|
|
||||||
setFocused(statusId.value)
|
|
||||||
}
|
|
||||||
hoisted.undive = undive
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...hoisted,
|
hide,
|
||||||
isLinearView,
|
isLinearView,
|
||||||
isTreeView,
|
isTreeView,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
conversation,
|
conversation,
|
||||||
hide,
|
|
||||||
collapsable,
|
|
||||||
mobileLayout,
|
mobileLayout,
|
||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
isPage,
|
isPage,
|
||||||
status,
|
status,
|
||||||
loadStatusError,
|
loadStatusError,
|
||||||
maybeFocused,
|
focused,
|
||||||
setFocused,
|
setFocused,
|
||||||
showOtherRepliesButtonBelowStatus,
|
showOtherRepliesButtonBelowStatus,
|
||||||
showOtherRepliesButtonInsideStatus,
|
|
||||||
onStatusSuspendStateChange,
|
onStatusSuspendStateChange,
|
||||||
getStatusClasses,
|
getStatusClasses,
|
||||||
getReplies,
|
getReplies,
|
||||||
currentAncestors,
|
currentAncestors,
|
||||||
statusId,
|
statusId,
|
||||||
collapsable,
|
|
||||||
pinnedStatusIdsObject,
|
|
||||||
inProfile,
|
|
||||||
profileUserId,
|
|
||||||
virtualHidden,
|
virtualHidden,
|
||||||
shouldShowAncestors,
|
shouldShowAncestors,
|
||||||
shouldShowAllConversationButton,
|
shouldShowAllConversationButton,
|
||||||
|
|
@ -489,5 +453,5 @@ export default {
|
||||||
diveIntoStatus,
|
diveIntoStatus,
|
||||||
showThreadRecursively,
|
showThreadRecursively,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,19 @@
|
||||||
</template>
|
</template>
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
v-if="collapsable"
|
v-if="!isPage"
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
@click.prevent="toggleExpanded"
|
@click.prevent="toggleExpanded"
|
||||||
>
|
>
|
||||||
{{ $t('timeline.collapse') }}
|
{{ $t('timeline.collapse') }}
|
||||||
</button>
|
</button>
|
||||||
<QuickFilterSettings
|
<QuickFilterSettings
|
||||||
v-if="!collapsable && mobileLayout"
|
v-if="isPage && mobileLayout"
|
||||||
:conversation="true"
|
:conversation="true"
|
||||||
class="rightside-button"
|
class="rightside-button"
|
||||||
/>
|
/>
|
||||||
<QuickViewSettings
|
<QuickViewSettings
|
||||||
v-if="!collapsable"
|
v-if="isPage"
|
||||||
:conversation="true"
|
:conversation="true"
|
||||||
class="rightside-button"
|
class="rightside-button"
|
||||||
/>
|
/>
|
||||||
|
|
@ -86,93 +86,61 @@
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
<DynamicScroller
|
<div
|
||||||
v-if="shouldShowAncestors"
|
v-if="shouldShowAncestors"
|
||||||
class="thread-ancestors"
|
class="thread-ancestors"
|
||||||
:min-item-size="15"
|
|
||||||
:buffer="500"
|
|
||||||
:items="currentAncestors"
|
|
||||||
role="feed"
|
|
||||||
list-tag="article"
|
|
||||||
item-tag="article"
|
|
||||||
:item-class="{'thread-ancestor-has-other-replies': getReplies(status.id).size > 1, '-faded': shouldFadeAncestors, 'thread-ancestor': true }"
|
|
||||||
flow-mode
|
|
||||||
page-mode
|
|
||||||
>
|
>
|
||||||
<template #default="{ item: status, active }">
|
<article
|
||||||
<DynamicScrollerItem
|
v-for="status in currentAncestors"
|
||||||
:item="status"
|
class="thread-ancestor"
|
||||||
:active="active"
|
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
class="conversation-status panel-body"
|
||||||
|
:class="getStatusClasses(status)"
|
||||||
|
|
||||||
|
:status-id="status.id"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
|
:focused="focused === status.id"
|
||||||
|
can-dive
|
||||||
|
|
||||||
|
@goto="setFocused"
|
||||||
|
@dive="() => diveIntoStatus(status.id)"
|
||||||
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).size > 1"
|
||||||
|
class="thread-ancestor-dive-box"
|
||||||
>
|
>
|
||||||
<Status
|
|
||||||
class="conversation-status panel-body"
|
|
||||||
:class="getStatusClasses(status, active)"
|
|
||||||
|
|
||||||
:status-id="status.id"
|
|
||||||
:replies="getReplies(status.id)"
|
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
|
||||||
:focused="maybeFocused === status.id"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
|
||||||
:in-profile="inProfile"
|
|
||||||
:in-conversation="isExpanded"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
|
||||||
can-dive
|
|
||||||
|
|
||||||
@goto="setFocused"
|
|
||||||
@dive="() => diveIntoStatus(status.id)"
|
|
||||||
@suspendable-state-change="onStatusSuspendStateChange"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).size > 1"
|
class="thread-ancestor-dive-box-inner"
|
||||||
class="thread-ancestor-dive-box"
|
|
||||||
>
|
>
|
||||||
<div
|
<i18n-t
|
||||||
class="thread-ancestor-dive-box-inner"
|
tag="button"
|
||||||
|
scope="global"
|
||||||
|
keypath="status.ancestor_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
>
|
>
|
||||||
<i18n-t
|
<template #icon>
|
||||||
tag="button"
|
<FAIcon
|
||||||
scope="global"
|
icon="angle-double-right"
|
||||||
keypath="status.ancestor_follow_with_icon"
|
/>
|
||||||
class="button-unstyled -link thread-tree-show-replies-button"
|
</template>
|
||||||
@click.prevent="diveIntoStatus(status.id)"
|
<template #text>
|
||||||
>
|
<span>
|
||||||
<template #icon>
|
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).size - 1).size - 1 }) }}
|
||||||
<FAIcon
|
</span>
|
||||||
icon="angle-double-right"
|
</template>
|
||||||
/>
|
</i18n-t>
|
||||||
</template>
|
|
||||||
<template #text>
|
|
||||||
<span>
|
|
||||||
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).size - 1).size - 1 }) }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicScrollerItem>
|
</div>
|
||||||
</template>
|
</article>
|
||||||
</DynamicScroller>
|
</div>
|
||||||
<ThreadTree
|
<ThreadTree
|
||||||
:key="currentStatus.id"
|
|
||||||
:depth="0"
|
|
||||||
|
|
||||||
:status-id="currentStatus.id"
|
:status-id="currentStatus.id"
|
||||||
:in-profile="inProfile"
|
:depth="0"
|
||||||
:conversation="conversation"
|
|
||||||
:collapsable="collapsable"
|
|
||||||
:is-expanded="isExpanded"
|
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
|
|
||||||
:replies="replies"
|
|
||||||
:focused="maybeFocused"
|
|
||||||
|
|
||||||
:thread-display="threadDisplay"
|
|
||||||
:thread-display-default="threadDisplayDefault"
|
|
||||||
:can-dive="isExpanded"
|
|
||||||
|
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@dive="diveIntoStatus"
|
@dive="diveIntoStatus"
|
||||||
|
|
@ -201,17 +169,11 @@
|
||||||
<Status
|
<Status
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
class="conversation-status"
|
class="conversation-status"
|
||||||
:class="getStatusClasses(status, active)"
|
:class="getStatusClasses(status)"
|
||||||
:status-id="status.id"
|
:status-id="status.id"
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
:focused="focused === status.id || focused === status.retweeted_status?.id"
|
||||||
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
|
||||||
:in-profile="inProfile"
|
|
||||||
:in-conversation="isExpanded"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
|
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<Status
|
<Status
|
||||||
v-if="shouldDisplayQuote"
|
v-if="shouldDisplayQuote"
|
||||||
:statusoid="quotedStatus"
|
:statusoid="quotedStatus"
|
||||||
:in-quote="true"
|
in-quote
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
<p
|
<p
|
||||||
|
|
|
||||||
|
|
@ -98,19 +98,12 @@ const Status = {
|
||||||
statusoid: Object,
|
statusoid: Object,
|
||||||
replies: Set,
|
replies: Set,
|
||||||
|
|
||||||
expandable: Boolean,
|
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
compact: Boolean,
|
compact: Boolean,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
noHeading: Boolean,
|
noHeading: Boolean,
|
||||||
inlineExpanded: Boolean,
|
|
||||||
inProfile: Boolean,
|
|
||||||
inConversation: Boolean,
|
|
||||||
inQuote: Boolean,
|
inQuote: Boolean,
|
||||||
|
|
||||||
profileUserId: String,
|
|
||||||
showOtherRepliesAsButton: Boolean,
|
|
||||||
canDive: Boolean,
|
|
||||||
ignoreMute: Boolean,
|
ignoreMute: Boolean,
|
||||||
|
|
||||||
threadDisplayState: String,
|
threadDisplayState: String,
|
||||||
|
|
@ -122,6 +115,11 @@ const Status = {
|
||||||
'suspendableStateChange',
|
'suspendableStateChange',
|
||||||
'heightChange',
|
'heightChange',
|
||||||
],
|
],
|
||||||
|
inject: {
|
||||||
|
profileUserId: { default: null },
|
||||||
|
isPage: { default: false },
|
||||||
|
isExpanded: { default: false },
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
replying: false,
|
replying: false,
|
||||||
|
|
@ -139,6 +137,12 @@ const Status = {
|
||||||
status() {
|
status() {
|
||||||
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
|
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
|
||||||
},
|
},
|
||||||
|
inConversation() {
|
||||||
|
return this.isExpanded
|
||||||
|
},
|
||||||
|
inProfile() {
|
||||||
|
return this.profileUserId != null
|
||||||
|
},
|
||||||
// Status repeated
|
// Status repeated
|
||||||
repeatedStatus() {
|
repeatedStatus() {
|
||||||
if (this.status.retweeted_status === undefined) return undefined
|
if (this.status.retweeted_status === undefined) return undefined
|
||||||
|
|
@ -168,6 +172,9 @@ const Status = {
|
||||||
simpleTree() {
|
simpleTree() {
|
||||||
return !this.mergedConfig.conversationTreeAdvanced
|
return !this.mergedConfig.conversationTreeAdvanced
|
||||||
},
|
},
|
||||||
|
showOtherRepliesAsButton() {
|
||||||
|
return this.mergedConfig.conversationOtherRepliesButton === 'inside'
|
||||||
|
},
|
||||||
showReasonMutedThread() {
|
showReasonMutedThread() {
|
||||||
return (
|
return (
|
||||||
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
|
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
v-if="!hideStatus"
|
v-if="!hideStatus"
|
||||||
ref="root"
|
ref="root"
|
||||||
class="Status"
|
class="Status"
|
||||||
:class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]"
|
:class="[{ '-focused': focused }, { '-conversation': !isPage && isExpanded }]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="error"
|
||||||
|
|
@ -199,7 +199,7 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="expandable && !isPreview"
|
v-if="!isExpanded && !isPreview"
|
||||||
class="button-unstyled"
|
class="button-unstyled"
|
||||||
:title="$t('status.expand')"
|
:title="$t('status.expand')"
|
||||||
@click.prevent="toggleExpanded"
|
@click.prevent="toggleExpanded"
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDive && !simpleTree"
|
v-if="isExpanded && !simpleTree"
|
||||||
class="button-unstyled"
|
class="button-unstyled"
|
||||||
:title="$t('status.show_only_conversation_under_this')"
|
:title="$t('status.show_only_conversation_under_this')"
|
||||||
@click.prevent="$emit('dive')"
|
@click.prevent="$emit('dive')"
|
||||||
|
|
|
||||||
|
|
@ -293,8 +293,10 @@ export const BUTTONS = [
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
[
|
[
|
||||||
useInstanceStore().server,
|
useInstanceStore().server,
|
||||||
router.resolve({ name: 'conversation', params: { statusId: status.id } })
|
router.resolve({
|
||||||
.href,
|
name: 'conversation',
|
||||||
|
params: { statusId: status.id },
|
||||||
|
}).href,
|
||||||
].join(''),
|
].join(''),
|
||||||
)
|
)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faAngleDoubleDown,
|
faAngleDoubleDown,
|
||||||
faAngleDoubleRight,
|
faAngleDoubleRight,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
|
||||||
|
|
||||||
library.add(faAngleDoubleDown, faAngleDoubleRight)
|
library.add(faAngleDoubleDown, faAngleDoubleRight)
|
||||||
|
|
||||||
const ThreadTree = {
|
const ThreadTree = {
|
||||||
|
|
@ -13,20 +13,7 @@ const ThreadTree = {
|
||||||
name: 'ThreadTree',
|
name: 'ThreadTree',
|
||||||
props: {
|
props: {
|
||||||
statusId: String,
|
statusId: String,
|
||||||
inProfile: Boolean,
|
|
||||||
collapsable: Boolean,
|
|
||||||
isExpanded: Boolean,
|
|
||||||
pinnedStatusIdsObject: Object,
|
|
||||||
profileUserId: String,
|
|
||||||
|
|
||||||
depth: Number,
|
depth: Number,
|
||||||
conversation: Array,
|
|
||||||
focused: String,
|
|
||||||
replies: Map,
|
|
||||||
|
|
||||||
canDive: Boolean,
|
|
||||||
threadDisplay: Map,
|
|
||||||
threadDisplayDefault: Map,
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'suspendableStateChange',
|
'suspendableStateChange',
|
||||||
|
|
@ -36,6 +23,15 @@ const ThreadTree = {
|
||||||
'toggleExpanded',
|
'toggleExpanded',
|
||||||
'showThreadRecursively',
|
'showThreadRecursively',
|
||||||
],
|
],
|
||||||
|
inject: [
|
||||||
|
'conversation',
|
||||||
|
'focused',
|
||||||
|
'replies',
|
||||||
|
'threadDisplay',
|
||||||
|
'threadDisplayDefault',
|
||||||
|
'isExpanded',
|
||||||
|
'isPage',
|
||||||
|
],
|
||||||
computed: {
|
computed: {
|
||||||
currentReplies() {
|
currentReplies() {
|
||||||
return [...this.getReplies(this.statusId)].map(({ id }) => id)
|
return [...this.getReplies(this.statusId)].map(({ id }) => id)
|
||||||
|
|
@ -44,9 +40,14 @@ const ThreadTree = {
|
||||||
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
|
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
|
||||||
},
|
},
|
||||||
threadShowing() {
|
threadShowing() {
|
||||||
const result = this.threadDisplay.get(this.statusId) ?? this.threadDisplayDefault.get(this.statusId)
|
const result =
|
||||||
|
this.threadDisplay.get(this.statusId) ??
|
||||||
|
this.threadDisplayDefault.get(this.statusId)
|
||||||
return result === 'showing'
|
return result === 'showing'
|
||||||
},
|
},
|
||||||
|
canDive() {
|
||||||
|
return this.isExpanded
|
||||||
|
},
|
||||||
totalReplyCount() {
|
totalReplyCount() {
|
||||||
const sizes = {}
|
const sizes = {}
|
||||||
const subTreeSizeFor = (id) => {
|
const subTreeSizeFor = (id) => {
|
||||||
|
|
@ -92,7 +93,7 @@ const ThreadTree = {
|
||||||
getReplies(id) {
|
getReplies(id) {
|
||||||
return this.replies.get(id) ?? new Set()
|
return this.replies.get(id) ?? new Set()
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThreadTree
|
export default ThreadTree
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,12 @@
|
||||||
<article class="thread-tree">
|
<article class="thread-tree">
|
||||||
<Status
|
<Status
|
||||||
:key="statusId"
|
:key="statusId"
|
||||||
ref="statusComponent"
|
class="conversation-status conversation-status-treeview panel-body"
|
||||||
:status-id="statusId"
|
:status-id="statusId"
|
||||||
:replies="getReplies(statusId)"
|
:replies="getReplies(statusId)"
|
||||||
:inline-expanded="collapsable && isExpanded"
|
|
||||||
:expandable="!isExpanded"
|
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
|
||||||
:in-conversation="isExpanded"
|
|
||||||
:focused="focused === statusId"
|
:focused="focused === statusId"
|
||||||
:in-profile="inProfile"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
class="conversation-status conversation-status-treeview panel-body"
|
|
||||||
|
|
||||||
:thread-display-state="threadDisplay.get(statusId)"
|
:thread-display-state="threadDisplay.get(statusId)"
|
||||||
:can-dive="canDive"
|
|
||||||
|
|
||||||
@dive="$emit('dive', statusId)"
|
@dive="$emit('dive', statusId)"
|
||||||
@goto="$emit('goto', statusId)"
|
@goto="$emit('goto', statusId)"
|
||||||
|
|
@ -30,24 +22,9 @@
|
||||||
<ThreadTree
|
<ThreadTree
|
||||||
v-for="replyStatusId in currentReplies"
|
v-for="replyStatusId in currentReplies"
|
||||||
:key="replyStatusId"
|
:key="replyStatusId"
|
||||||
ref="childComponent"
|
|
||||||
:depth="depth + 1"
|
:depth="depth + 1"
|
||||||
:status-id="replyStatusId"
|
:status-id="replyStatusId"
|
||||||
|
|
||||||
:in-profile="inProfile"
|
|
||||||
:conversation="conversation"
|
|
||||||
:collapsable="collapsable"
|
|
||||||
:is-expanded="isExpanded"
|
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
|
|
||||||
:replies="replies"
|
|
||||||
:focused="focused"
|
|
||||||
|
|
||||||
:thread-display="threadDisplay"
|
|
||||||
:thread-display-default="threadDisplayDefault"
|
|
||||||
|
|
||||||
:can-dive="canDive"
|
|
||||||
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)"
|
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)"
|
||||||
@goto="(e) => $emit('goto', e)"
|
@goto="(e) => $emit('goto', e)"
|
||||||
@dive="(e) => $emit('dive', e)"
|
@dive="(e) => $emit('dive', e)"
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ const Timeline = {
|
||||||
blockingClicks: false,
|
blockingClicks: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
profileUserId: this.inProfile && this.timelineRef.argument,
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
ScrollTopButton,
|
ScrollTopButton,
|
||||||
Conversation,
|
Conversation,
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,6 @@
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
:status-id="status.id"
|
:status-id="status.id"
|
||||||
:in-profile="inProfile"
|
|
||||||
:profile-user-id="timelineRef.argument"
|
|
||||||
collapsable
|
collapsable
|
||||||
/>
|
/>
|
||||||
</DynamicScrollerItem>
|
</DynamicScrollerItem>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue