Merge pull request 'more-fixes' (#3505) from more-fixes into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3505
This commit is contained in:
commit
2835da1e38
71 changed files with 755 additions and 577 deletions
|
|
@ -157,7 +157,7 @@ e2e-pleroma:
|
||||||
variables:
|
variables:
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||||
FF_NETWORK_PER_BUILD: "true"
|
FF_NETWORK_PER_BUILD: "true"
|
||||||
PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
|
PLEROMA_IMAGE: git.pleroma.social/pleroma/pleroma:stable
|
||||||
POSTGRES_USER: pleroma
|
POSTGRES_USER: pleroma
|
||||||
POSTGRES_PASSWORD: pleroma
|
POSTGRES_PASSWORD: pleroma
|
||||||
POSTGRES_DB: pleroma
|
POSTGRES_DB: pleroma
|
||||||
|
|
|
||||||
0
changelog.d/more-fixes.skip
Normal file
0
changelog.d/more-fixes.skip
Normal file
|
|
@ -12,7 +12,7 @@ services:
|
||||||
retries: 30
|
retries: 30
|
||||||
|
|
||||||
pleroma:
|
pleroma:
|
||||||
image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
|
image: ${PLEROMA_IMAGE:-git.pleroma.social/pleroma/pleroma:stable}
|
||||||
environment:
|
environment:
|
||||||
DB_USER: pleroma
|
DB_USER: pleroma
|
||||||
DB_PASS: pleroma
|
DB_PASS: pleroma
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue'
|
import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue'
|
||||||
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||||
|
import GlobalError from 'src/components/global_error/global_error.vue'
|
||||||
import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue'
|
import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue'
|
||||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import MobileNav from 'src/components/mobile_nav/mobile_nav.vue'
|
import MobileNav from 'src/components/mobile_nav/mobile_nav.vue'
|
||||||
|
|
@ -68,6 +69,7 @@ export default {
|
||||||
() =>
|
() =>
|
||||||
import('src/components/status_history_modal/status_history_modal.vue'),
|
import('src/components/status_history_modal/status_history_modal.vue'),
|
||||||
),
|
),
|
||||||
|
GlobalError,
|
||||||
GlobalNoticeList,
|
GlobalNoticeList,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
|
||||||
19
src/App.scss
19
src/App.scss
|
|
@ -144,6 +144,25 @@ h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--roundness);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
|
||||||
|
& pre,
|
||||||
|
pre & {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pre {
|
||||||
|
white-space: pre;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.iconLetter {
|
.iconLetter {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
<StatusHistoryModal v-if="editingAvailable" />
|
<StatusHistoryModal v-if="editingAvailable" />
|
||||||
<SettingsModal :class="layoutModalClass" />
|
<SettingsModal :class="layoutModalClass" />
|
||||||
<UpdateNotification />
|
<UpdateNotification />
|
||||||
|
<GlobalError />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -435,7 +435,7 @@ export const downloadRemoteEmojiPackZIP = ({
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: EMOJI_PACKS_DL_REMOTE_ZIP_URL,
|
url: EMOJI_PACKS_DL_REMOTE_ZIP_URL,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
payload: data,
|
formData: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,7 +460,7 @@ export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: EMOJI_UPDATE_FILE_URL(packName),
|
url: EMOJI_UPDATE_FILE_URL(packName),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
payload: data,
|
formData: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.serv
|
||||||
|
|
||||||
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
||||||
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||||
const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit }) =>
|
const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit } = {}) =>
|
||||||
`/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}`
|
`/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}`
|
||||||
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
||||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ export const paramsString = (params = {}) => {
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (entries.length === 0) return ''
|
|
||||||
|
|
||||||
const arrays = []
|
const arrays = []
|
||||||
const nonArrays = []
|
const nonArrays = []
|
||||||
|
|
||||||
|
|
@ -48,6 +46,8 @@ export const paramsString = (params = {}) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (nonArrays.length + arrays.length === 0) return ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
'?' +
|
'?' +
|
||||||
[
|
[
|
||||||
|
|
@ -71,6 +71,7 @@ export const promisedRequest = async ({
|
||||||
url,
|
url,
|
||||||
payload,
|
payload,
|
||||||
formData,
|
formData,
|
||||||
|
cache,
|
||||||
credentials,
|
credentials,
|
||||||
headers = {},
|
headers = {},
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -87,6 +88,10 @@ export const promisedRequest = async ({
|
||||||
options.headers['Content-Type'] = 'application/json'
|
options.headers['Content-Type'] = 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cache) {
|
||||||
|
options.cache = cache
|
||||||
|
}
|
||||||
|
|
||||||
if (formData || payload) {
|
if (formData || payload) {
|
||||||
options.body = formData || JSON.stringify(payload)
|
options.body = formData || JSON.stringify(payload)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,6 @@ export const unmuteConversation = ({ id, credentials }) =>
|
||||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
export const vote = ({ pollId, choices, credentials }) => {
|
export const vote = ({ pollId, choices, credentials }) => {
|
||||||
const form = new FormData()
|
|
||||||
form.append('choices', choices)
|
|
||||||
|
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
|
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -467,6 +467,16 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
|
app.config.errorHandler = (error, instance, info) => {
|
||||||
|
console.error(
|
||||||
|
'Global Vue Error Handler caught an error:',
|
||||||
|
error,
|
||||||
|
instance,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
useInterfaceStore().setGlobalError({ error, instance, info })
|
||||||
|
}
|
||||||
|
|
||||||
const waitForAllStoresToLoad = async () => {
|
const waitForAllStoresToLoad = async () => {
|
||||||
// the stores that do not persist technically do not need to be awaited here,
|
// the stores that do not persist technically do not need to be awaited here,
|
||||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import AuthForm from 'src/components/auth_form/auth_form.js'
|
||||||
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
|
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
|
||||||
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
|
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
|
||||||
import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
|
||||||
|
|
@ -147,9 +148,7 @@ export default (store) => {
|
||||||
{
|
{
|
||||||
name: 'login',
|
name: 'login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
component: defineAsyncComponent(
|
component: AuthForm,
|
||||||
() => import('src/components/auth_form/auth_form.js'),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'shout-panel',
|
name: 'shout-panel',
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const Attachment = {
|
||||||
'shiftDn',
|
'shiftDn',
|
||||||
'edit',
|
'edit',
|
||||||
],
|
],
|
||||||
|
emits: ['play', 'pause', 'naturalSizeLoad'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
localDescription: this.description || this.attachment.description,
|
localDescription: this.description || this.attachment.description,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.basic-user-card {
|
.basic-user-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0;
|
flex: 1 1 10em;
|
||||||
|
min-width: 1em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from './chat_layout_utils.js'
|
} from './chat_layout_utils.js'
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -116,8 +117,8 @@ const Chat = {
|
||||||
'currentChat',
|
'currentChat',
|
||||||
'currentChatMessageService',
|
'currentChatMessageService',
|
||||||
'findOpenedChatByRecipientId',
|
'findOpenedChatByRecipientId',
|
||||||
'mergedConfig',
|
|
||||||
]),
|
]),
|
||||||
|
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||||
...mapPiniaState(useInterfaceStore, {
|
...mapPiniaState(useInterfaceStore, {
|
||||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||||
}),
|
}),
|
||||||
|
|
@ -381,7 +382,7 @@ const Chat = {
|
||||||
if (retriesLeft <= 0) return
|
if (retriesLeft <= 0) return
|
||||||
|
|
||||||
sendChatMessage({
|
sendChatMessage({
|
||||||
params,
|
...params,
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
|
import { chats } from 'src/api/chats.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
|
@ -24,10 +26,10 @@ const chatNew = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
const { chats } = await chats({
|
const { chatList } = await chats({
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
})
|
})
|
||||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
chatList.forEach((chat) => this.suggestions.push(chat.account))
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
users() {
|
users() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<conversation
|
<Conversation
|
||||||
:collapsable="false"
|
|
||||||
is-page="true"
|
|
||||||
:status-id="statusId"
|
:status-id="statusId"
|
||||||
|
is-page
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,25 +55,56 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = {
|
const conversation = {
|
||||||
|
props: {
|
||||||
|
statusId: {
|
||||||
|
// Main thing
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collapsable: {
|
||||||
|
// Whether conversation can be collapsed
|
||||||
|
// i.e. when it's not a page
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isPage: {
|
||||||
|
// Whether conversation is rendered as a standalone page
|
||||||
|
// as opposed to embedded into a timeline
|
||||||
|
type: Boolean,
|
||||||
|
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: {
|
||||||
|
// Whether conversation is suspended. Controls rendering of statuses
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
focused: null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
statusContentPropertiesObject: {},
|
|
||||||
inlineDivePosition: null,
|
inlineDivePosition: null,
|
||||||
loadStatusError: null,
|
loadStatusError: null,
|
||||||
|
unsuspendibleIds: new Set(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'statusId',
|
|
||||||
'collapsable',
|
|
||||||
'isPage',
|
|
||||||
'pinnedStatusIdsObject',
|
|
||||||
'inProfile',
|
|
||||||
'profileUserId',
|
|
||||||
'virtualHidden',
|
|
||||||
],
|
|
||||||
created() {
|
created() {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
|
@ -118,16 +149,7 @@ const conversation = {
|
||||||
return this.otherRepliesButtonPosition === 'inside'
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
},
|
},
|
||||||
suspendable() {
|
suspendable() {
|
||||||
if (this.isTreeView) {
|
return this.unsuspendibleIds.size > 0
|
||||||
return Object.entries(this.statusContentProperties).every(
|
|
||||||
([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
|
||||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hideStatus() {
|
hideStatus() {
|
||||||
return this.virtualHidden && this.suspendable
|
return this.virtualHidden && this.suspendable
|
||||||
|
|
@ -364,36 +386,11 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
statusContentProperties() {
|
|
||||||
return this.conversation.reduce((a, k) => {
|
|
||||||
const id = k.id
|
|
||||||
const props = (() => {
|
|
||||||
const def = {
|
|
||||||
showingTall: false,
|
|
||||||
expandingSubject: false,
|
|
||||||
showingLongSubject: false,
|
|
||||||
isReplying: false,
|
|
||||||
mediaPlaying: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.statusContentPropertiesObject[id]) {
|
|
||||||
return {
|
|
||||||
...def,
|
|
||||||
...this.statusContentPropertiesObject[id],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
})()
|
|
||||||
|
|
||||||
a[id] = props
|
|
||||||
return a
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
canDive() {
|
canDive() {
|
||||||
return this.isTreeView && this.isExpanded
|
return this.isTreeView && this.isExpanded
|
||||||
},
|
},
|
||||||
maybeHighlight() {
|
maybeFocused() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.focused : null
|
||||||
},
|
},
|
||||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
|
|
@ -417,7 +414,7 @@ const conversation = {
|
||||||
oldConversationId &&
|
oldConversationId &&
|
||||||
newConversationId === oldConversationId
|
newConversationId === oldConversationId
|
||||||
) {
|
) {
|
||||||
this.setHighlight(this.originalStatusId)
|
this.setFocused(this.originalStatusId)
|
||||||
} else {
|
} else {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
}
|
}
|
||||||
|
|
@ -445,7 +442,7 @@ const conversation = {
|
||||||
}).then(({ data: { ancestors, descendants } }) => {
|
}).then(({ data: { ancestors, descendants } }) => {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||||
this.setHighlight(this.originalStatusId)
|
this.setFocused(this.originalStatusId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.loadStatusError = null
|
this.loadStatusError = null
|
||||||
|
|
@ -463,18 +460,12 @@ const conversation = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isFocused(id) {
|
|
||||||
return this.isExpanded && id === this.highlight
|
|
||||||
},
|
|
||||||
getReplies(id) {
|
getReplies(id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
getHighlight() {
|
setFocused(id) {
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
setHighlight(id) {
|
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.focused = id
|
||||||
|
|
||||||
if (!this.streamingEnabled) {
|
if (!this.streamingEnabled) {
|
||||||
this.$store.dispatch('fetchStatus', id)
|
this.$store.dispatch('fetchStatus', id)
|
||||||
|
|
@ -514,22 +505,6 @@ const conversation = {
|
||||||
showThreadRecursively(id) {
|
showThreadRecursively(id) {
|
||||||
this.setThreadDisplayRecursively(id, 'showing')
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
},
|
},
|
||||||
setStatusContentProperty(id, name, value) {
|
|
||||||
this.statusContentPropertiesObject = {
|
|
||||||
...this.statusContentPropertiesObject,
|
|
||||||
[id]: {
|
|
||||||
...this.statusContentPropertiesObject[id],
|
|
||||||
[name]: value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleStatusContentProperty(id, name) {
|
|
||||||
this.setStatusContentProperty(
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
!this.statusContentProperties[id][name],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leastVisibleAncestor(id) {
|
leastVisibleAncestor(id) {
|
||||||
let cur = id
|
let cur = id
|
||||||
let parent = this.parentOf(cur)
|
let parent = this.parentOf(cur)
|
||||||
|
|
@ -555,7 +530,7 @@ const conversation = {
|
||||||
// only used when we are not on a page
|
// only used when we are not on a page
|
||||||
undive() {
|
undive() {
|
||||||
this.inlineDivePosition = null
|
this.inlineDivePosition = null
|
||||||
this.setHighlight(this.statusId)
|
this.setFocused(this.statusId)
|
||||||
},
|
},
|
||||||
tryScrollTo(id) {
|
tryScrollTo(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|
@ -573,7 +548,7 @@ const conversation = {
|
||||||
// contain scrolling calls, as we do not want the page to jump
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
// when we scroll with an expanded conversation.
|
// when we scroll with an expanded conversation.
|
||||||
//
|
//
|
||||||
// Now the method is to rely solely on the `highlight` watcher
|
// Now the method is to rely solely on the `focused` watcher
|
||||||
// in `status` components.
|
// in `status` components.
|
||||||
// In linear views, all statuses are rendered at all times, but
|
// In linear views, all statuses are rendered at all times, but
|
||||||
// in tree views, it is possible that a change in active status
|
// in tree views, it is possible that a change in active status
|
||||||
|
|
@ -581,9 +556,9 @@ const conversation = {
|
||||||
// status becomes an ancestor status, and thus they will be
|
// status becomes an ancestor status, and thus they will be
|
||||||
// different).
|
// different).
|
||||||
// Here, let the components be rendered first, in order to trigger
|
// Here, let the components be rendered first, in order to trigger
|
||||||
// the `highlight` watcher.
|
// the `focused` watcher.
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.setHighlight(id)
|
this.setFocused(id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToCurrent() {
|
goToCurrent() {
|
||||||
|
|
@ -629,6 +604,13 @@ const conversation = {
|
||||||
this.undive()
|
this.undive()
|
||||||
this.threadDisplayStatusObject = {}
|
this.threadDisplayStatusObject = {}
|
||||||
},
|
},
|
||||||
|
onStatusSuspendStateChange({ id, suspend }) {
|
||||||
|
if (!suspend) {
|
||||||
|
this.unsuspendibleIds.add(id)
|
||||||
|
} else {
|
||||||
|
this.unsuspendibleIds.delete(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:focused="isFocused(status.id)"
|
:focused="maybeFocused === status.id"
|
||||||
:highlight="getHighlight()"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
|
|
@ -105,21 +104,11 @@
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
:simple-tree="treeViewIsSimple"
|
:simple-tree="treeViewIsSimple"
|
||||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
:dive="() => diveIntoStatus(status.id)"
|
can-dive
|
||||||
|
|
||||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
@goto="setFocused"
|
||||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
@dive="() => diveIntoStatus(status.id)"
|
||||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
|
||||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
|
||||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
|
||||||
:controlled-replying="statusContentProperties[status.id].replying"
|
|
||||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
|
||||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
|
||||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
|
||||||
|
|
||||||
@goto="setHighlight"
|
|
||||||
@toggle-expanded="toggleExpanded"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
|
@ -150,7 +139,7 @@
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<thread-tree
|
<ThreadTree
|
||||||
v-for="status in showingTopLevel"
|
v-for="status in showingTopLevel"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
|
|
@ -164,22 +153,20 @@
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:is-focused-function="isFocused"
|
|
||||||
:get-replies="getReplies"
|
:get-replies="getReplies"
|
||||||
:highlight="maybeHighlight"
|
:focused="maybeFocused"
|
||||||
:set-highlight="setHighlight"
|
|
||||||
:toggle-expanded="toggleExpanded"
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
:simple="treeViewIsSimple"
|
:simple="treeViewIsSimple"
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
:show-thread-recursively="showThreadRecursively"
|
||||||
:total-reply-count="totalReplyCount"
|
:total-reply-count="totalReplyCount"
|
||||||
:total-reply-depth="totalReplyDepth"
|
:total-reply-depth="totalReplyDepth"
|
||||||
:status-content-properties="statusContentProperties"
|
:can-dive="canDive"
|
||||||
:set-status-content-property="setStatusContentProperty"
|
|
||||||
:toggle-status-content-property="toggleStatusContentProperty"
|
@goto="setFocused"
|
||||||
:dive="canDive ? diveIntoStatus : undefined"
|
@dive="diveIntoStatus"
|
||||||
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -187,7 +174,7 @@
|
||||||
class="thread-body"
|
class="thread-body"
|
||||||
>
|
>
|
||||||
<article>
|
<article>
|
||||||
<status
|
<Status
|
||||||
v-for="status in conversation"
|
v-for="status in conversation"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
|
|
@ -196,16 +183,16 @@
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:focused="isFocused(status.id)"
|
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
||||||
:highlight="getHighlight()"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setFocused"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -69,15 +69,6 @@ const Draft = {
|
||||||
localCollapseSubjectDefault() {
|
localCollapseSubjectDefault() {
|
||||||
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
||||||
},
|
},
|
||||||
nsfwClickthrough() {
|
|
||||||
if (!this.draft.nsfw) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.draft.summary && this.localCollapseSubjectDefault) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editing(newVal) {
|
editing(newVal) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
:params="params"
|
:params="params"
|
||||||
@posted="doCloseModal"
|
@posted="doCloseModal"
|
||||||
@draft-done="doCloseModal"
|
@draft-done="doCloseModal"
|
||||||
@can-close="doCloseModal"
|
@close-accepted="doCloseModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ export default (data) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
|
export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
const noPrefix = input.toLowerCase().substring(1)
|
||||||
return emojis
|
return emojis
|
||||||
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||||
.filter(
|
.filter(
|
||||||
(emoji) =>
|
(emoji) =>
|
||||||
emoji.names
|
emoji.names
|
||||||
.concat(emoji.keywords)
|
.concat(emoji.keywords)
|
||||||
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
|
.filter((kw) => kw.toLowerCase().includes(noPrefix)).length,
|
||||||
)
|
)
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
let score = 0
|
let score = 0
|
||||||
|
|
|
||||||
36
src/components/error_modal/error_modal.js
Normal file
36
src/components/error_modal/error_modal.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleXmark)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component emits the following events:
|
||||||
|
* cancelled, emitted when the action should not be performed;
|
||||||
|
* accepted, emitted when the action should be performed;
|
||||||
|
*
|
||||||
|
* The caller should close this dialog after receiving any of the two events.
|
||||||
|
*/
|
||||||
|
const ErrorModal = {
|
||||||
|
components: {
|
||||||
|
DialogModal,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
clearText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
recoverText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['clear', 'recover'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorModal
|
||||||
103
src/components/error_modal/error_modal.vue
Normal file
103
src/components/error_modal/error_modal.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<DialogModal
|
||||||
|
v-body-scroll-lock="true"
|
||||||
|
class="error-modal"
|
||||||
|
@cancel="onCancel"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span v-text="title ?? $t('general.generic_error')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<FAIcon
|
||||||
|
class="error-icon"
|
||||||
|
icon="circle-xmark"
|
||||||
|
size="3x"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
<div class="text">
|
||||||
|
<slot>
|
||||||
|
<p>
|
||||||
|
<strong><code v-text="error.name" /></strong>
|
||||||
|
{{ ' - ' }}
|
||||||
|
<span v-text="error.message" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>{{ $t('general.generic_error_details') }}</summary>
|
||||||
|
<code
|
||||||
|
class="stack pre"
|
||||||
|
v-text="error.stack"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="below">
|
||||||
|
<slot name="below" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<slot name="footerLeft" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="recoverText"
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="$emit('recover')"
|
||||||
|
v-text="recoverText"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="$emit('clear')"
|
||||||
|
v-text="clearText ?? $t('general.close')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./error_modal.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
.error-modal {
|
||||||
|
.error-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.below:not(:empty) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 50ch;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 3.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -27,8 +27,10 @@ const Gallery = {
|
||||||
return {
|
return {
|
||||||
sizes: {},
|
sizes: {},
|
||||||
hidingLong: true,
|
hidingLong: true,
|
||||||
|
playingMedia: new Set(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['play', 'pause'],
|
||||||
components: { Attachment },
|
components: { Attachment },
|
||||||
computed: {
|
computed: {
|
||||||
rows() {
|
rows() {
|
||||||
|
|
@ -115,11 +117,21 @@ const Gallery = {
|
||||||
return this.attachmentsDimensionalScore > 1
|
return this.attachmentsDimensionalScore > 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hasPlayingMedia() {
|
||||||
|
return this.playingMedia.size > 0
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNaturalSizeLoad({ id, width, height }) {
|
onNaturalSizeLoad({ id, width, height }) {
|
||||||
set(this.sizes, id, { width, height })
|
set(this.sizes, id, { width, height })
|
||||||
},
|
},
|
||||||
|
onMediaStateChange(playing, id) {
|
||||||
|
if (playing) {
|
||||||
|
this.playingMedia.add(id)
|
||||||
|
} else {
|
||||||
|
this.playingMedia.delete(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
rowStyle(row) {
|
rowStyle(row) {
|
||||||
if (row.audio) {
|
if (row.audio) {
|
||||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||||
|
|
@ -146,6 +158,15 @@ const Gallery = {
|
||||||
useMediaViewerStore().setMedia(this.attachments)
|
useMediaViewerStore().setMedia(this.attachments)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
hasPlayingMedia(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.$emit('play')
|
||||||
|
} else {
|
||||||
|
this.$emit('pause')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Gallery
|
export default Gallery
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
:style="itemStyle(attachment.id, row.items)"
|
:style="itemStyle(attachment.id, row.items)"
|
||||||
@set-media="onMedia"
|
@set-media="onMedia"
|
||||||
@natural-size-load="onNaturalSizeLoad"
|
@natural-size-load="onNaturalSizeLoad"
|
||||||
|
@play="() => onMediaStateChange(true, attachment.id)"
|
||||||
|
@pause="() => onMediaStateChange(false, attachment.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
57
src/components/global_error/global_error.js
Normal file
57
src/components/global_error/global_error.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
|
||||||
|
const GlobalError = {
|
||||||
|
components: {
|
||||||
|
ErrorModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/error_modal/error_modal.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
if (this.globalError == null) return null
|
||||||
|
return this.globalError.title && this.$t(this.globalError.title)
|
||||||
|
},
|
||||||
|
content() {
|
||||||
|
if (this.globalError == null) return null
|
||||||
|
if (this.globalError.content) {
|
||||||
|
return this.$t(this.globalError.content, [this.globalError.error])
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
details() {
|
||||||
|
if (this.globalError == null) return null
|
||||||
|
if (this.globalError.error != null) {
|
||||||
|
return (
|
||||||
|
this.globalError.error.toString() +
|
||||||
|
'\n\n' +
|
||||||
|
this.globalError.error.stack
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return this.globalError.details
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recoverText() {
|
||||||
|
if (this.globalError == null) return null
|
||||||
|
if (this.globalError.recoverText == null) return null
|
||||||
|
return this.$t(this.globalError.recoverText)
|
||||||
|
},
|
||||||
|
...mapState(useInterfaceStore, ['globalError']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clear() {
|
||||||
|
this.globalError.clear?.()
|
||||||
|
this.clearGlobalError()
|
||||||
|
},
|
||||||
|
recover() {
|
||||||
|
this.globalError.recover?.()
|
||||||
|
this.clearGlobalError()
|
||||||
|
},
|
||||||
|
...mapActions(useInterfaceStore, ['clearGlobalError']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalError
|
||||||
26
src/components/global_error/global_error.vue
Normal file
26
src/components/global_error/global_error.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<teleport to="#modal">
|
||||||
|
<ErrorModal
|
||||||
|
v-if="globalError"
|
||||||
|
:error="globalError.error"
|
||||||
|
:title="title"
|
||||||
|
:recover-text="recoverText"
|
||||||
|
@recover="recover"
|
||||||
|
@clear="clear"
|
||||||
|
>
|
||||||
|
<template v-if="content">
|
||||||
|
<p>{{ content }}</p>
|
||||||
|
|
||||||
|
<details v-if="details">
|
||||||
|
<summary>{{ $t('general.generic_error_details') }}</summary>
|
||||||
|
<code
|
||||||
|
class="stack pre"
|
||||||
|
v-text="details"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
</ErrorModal>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./global_error.js"></script>
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
class="list-member-management"
|
class="list-member-management"
|
||||||
:scrollable-tabs
|
scrollable-tabs
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="id || addedUserIds.size > 0"
|
v-if="id || addedUserIds.size > 0"
|
||||||
|
|
|
||||||
|
|
@ -152,9 +152,6 @@ const MobileNav = {
|
||||||
},
|
},
|
||||||
onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||||
this.notificationsAtTop = scrollTop > 0
|
this.notificationsAtTop = scrollTop > 0
|
||||||
if (scrollTop + clientHeight >= scrollHeight) {
|
|
||||||
this.$refs.notifications.fetchOlderNotifications()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Notifications teleport target -->
|
||||||
<div
|
<div
|
||||||
id="mobile-notifications"
|
id="mobile-notifications"
|
||||||
ref="mobileNotifications"
|
ref="mobileNotifications"
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.Status {
|
|
||||||
/* stylelint-disable-next-line declaration-no-important */
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
--emoji-size: 1em;
|
--emoji-size: 1em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Notification',
|
name: 'Notification',
|
||||||
selector: '.Notification',
|
selector: '.NotificationParent',
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Text',
|
'Text',
|
||||||
'Link',
|
'Link',
|
||||||
|
|
@ -9,5 +9,11 @@ export default {
|
||||||
'Avatar',
|
'Avatar',
|
||||||
'PollGraph',
|
'PollGraph',
|
||||||
],
|
],
|
||||||
defaultRules: [],
|
defaultRules: [
|
||||||
|
{
|
||||||
|
directives: {
|
||||||
|
background: '--bg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<article
|
<article
|
||||||
v-else
|
v-else
|
||||||
ref="root"
|
ref="root"
|
||||||
class="NotificationParent"
|
class="NotificationParent panel-body"
|
||||||
:class="{ '-expandable': expandable }"
|
:class="{ '-expandable': expandable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@
|
||||||
<NotificationFilters class="rightside-button" />
|
<NotificationFilters class="rightside-button" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="panel-body"
|
|
||||||
role="feed"
|
role="feed"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,24 @@
|
||||||
:class="{ '-compact': compact, '-apply': apply, '-mobile': mobile }"
|
:class="{ '-compact': compact, '-apply': apply, '-mobile': mobile }"
|
||||||
>
|
>
|
||||||
<div class="palette">
|
<div class="palette">
|
||||||
<ColorInput
|
<div
|
||||||
v-for="key in paletteKeys"
|
v-for="key in paletteKeys"
|
||||||
:key="key"
|
:key="key"
|
||||||
:name="key"
|
>
|
||||||
:model-value="props.modelValue[key]"
|
<ColorInput
|
||||||
:fallback="fallback(key)"
|
:name="key"
|
||||||
:label="$t('settings.style.themes3.palette.' + key)"
|
:model-value="props.modelValue[key]"
|
||||||
@update:model-value="value => updatePalette(key, value)"
|
:fallback="fallback(key)"
|
||||||
/>
|
:label="$t('settings.style.themes3.palette.' + key)"
|
||||||
|
@update:model-value="value => updatePalette(key, value)"
|
||||||
|
/>
|
||||||
|
<ContrastRatio
|
||||||
|
v-if="contrast?.[key]"
|
||||||
|
:show-ratio="true"
|
||||||
|
:contrast="contrast[key]"
|
||||||
|
/>
|
||||||
|
<div v-else>{{ ' ' }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button
|
||||||
|
|
@ -48,9 +57,14 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
|
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getContrastRatio,
|
||||||
|
hex2rgb,
|
||||||
|
} from 'src/services/color_convert/color_convert.js'
|
||||||
import {
|
import {
|
||||||
newExporter,
|
newExporter,
|
||||||
newImporter,
|
newImporter,
|
||||||
|
|
@ -106,6 +120,16 @@ const importPalette = () => {
|
||||||
paletteImporter.importData()
|
paletteImporter.importData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hints = (ratio) => ({
|
||||||
|
text: ratio.toPrecision(3) + ':1',
|
||||||
|
// AA level, AAA level
|
||||||
|
aa: ratio >= 4.5,
|
||||||
|
aaa: ratio >= 7,
|
||||||
|
// same but for 18pt+ texts
|
||||||
|
laa: ratio >= 3,
|
||||||
|
laaa: ratio >= 4.5,
|
||||||
|
})
|
||||||
|
|
||||||
const applyPalette = () => {
|
const applyPalette = () => {
|
||||||
emit('applyPalette', getExportedObject())
|
emit('applyPalette', getExportedObject())
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +138,19 @@ const mobile = computed(() => {
|
||||||
return useInterfaceStore().layoutType === 'mobile'
|
return useInterfaceStore().layoutType === 'mobile'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const contrast = computed(() => {
|
||||||
|
if (props.modelValue == null) return null
|
||||||
|
const bg = hex2rgb(props.modelValue.bg)
|
||||||
|
const text = hex2rgb(props.modelValue.text)
|
||||||
|
const link = hex2rgb(props.modelValue.link)
|
||||||
|
if (text == null || link == null) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: hints(getContrastRatio(bg, text)),
|
||||||
|
link: hints(getContrastRatio(bg, link)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const fallback = (key) => {
|
const fallback = (key) => {
|
||||||
if (key === 'accent') {
|
if (key === 'accent') {
|
||||||
return props.modelValue.link
|
return props.modelValue.link
|
||||||
|
|
@ -204,15 +241,6 @@ const updatePalette = (paletteKey, value) => {
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-input {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
label {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ const PostStatusForm = {
|
||||||
'resize',
|
'resize',
|
||||||
'mediaplay',
|
'mediaplay',
|
||||||
'mediapause',
|
'mediapause',
|
||||||
'can-close',
|
'close-accepted',
|
||||||
'update',
|
'update',
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -596,16 +596,18 @@ const PostStatusForm = {
|
||||||
? this.postHandler
|
? this.postHandler
|
||||||
: statusPoster.postStatus
|
: statusPoster.postStatus
|
||||||
|
|
||||||
postHandler(postingOptions).then((data) => {
|
postHandler(postingOptions)
|
||||||
if (!data.error) {
|
.then((data) => {
|
||||||
this.abandonDraft()
|
this.abandonDraft()
|
||||||
this.clearStatus()
|
this.clearStatus()
|
||||||
this.$emit('posted', data)
|
this.$emit('posted', data)
|
||||||
} else {
|
})
|
||||||
this.error = data.error
|
.catch((error) => {
|
||||||
}
|
this.error = error
|
||||||
this.posting = false
|
})
|
||||||
})
|
.finally(() => {
|
||||||
|
this.posting = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
previewStatus() {
|
previewStatus() {
|
||||||
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
||||||
|
|
@ -963,19 +965,19 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
requestClose() {
|
requestClose() {
|
||||||
if (!this.saveable) {
|
if (!this.saveable) {
|
||||||
this.$emit('can-close')
|
this.$emit('close-accepted')
|
||||||
} else {
|
} else {
|
||||||
this.$refs.draftCloser.requestClose()
|
this.$refs.draftCloser.requestClose()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveAndCloseDraft() {
|
saveAndCloseDraft() {
|
||||||
this.saveDraft().then(() => {
|
this.saveDraft().then(() => {
|
||||||
this.$emit('can-close')
|
this.$emit('close-accepted')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
discardAndCloseDraft() {
|
discardAndCloseDraft() {
|
||||||
this.abandonDraft().then(() => {
|
this.abandonDraft().then(() => {
|
||||||
this.$emit('can-close')
|
this.$emit('close-accepted')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addBeforeUnloadListener() {
|
addBeforeUnloadListener() {
|
||||||
|
|
|
||||||
|
|
@ -409,8 +409,8 @@
|
||||||
:remove-attachment="removeMediaFile"
|
:remove-attachment="removeMediaFile"
|
||||||
:shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile"
|
:shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile"
|
||||||
:shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile"
|
:shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile"
|
||||||
@play="$emit('mediaplay', attachment.id)"
|
@play="$emit('mediaplay', 'newStatus')"
|
||||||
@pause="$emit('mediapause', attachment.id)"
|
@pause="$emit('mediapause', 'newStatus')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
|
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,10 @@
|
||||||
<Status
|
<Status
|
||||||
v-for="status in visibleStatuses"
|
v-for="status in visibleStatuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
:collapsable="false"
|
|
||||||
:expandable="false"
|
:expandable="false"
|
||||||
:compact="false"
|
:compact="false"
|
||||||
class="search-result"
|
class="search-result"
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:no-heading="false"
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!loading && loaded && lastStatusFetchCount > 0"
|
v-if="!loading && loaded && lastStatusFetchCount > 0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
.AdminUserCard {
|
.AdminUserCard {
|
||||||
|
details {
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
max-height: 6.5em;
|
||||||
|
overflow-y: auto;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.right-side {
|
.right-side {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
|
|
||||||
|
|
@ -56,18 +56,14 @@
|
||||||
<Popover
|
<Popover
|
||||||
ref="additionalRemotePopover"
|
ref="additionalRemotePopover"
|
||||||
popover-class="emoji-tab-edit-popover popover-default"
|
popover-class="emoji-tab-edit-popover popover-default"
|
||||||
|
class="button button-default emoji-panel-additional-actions"
|
||||||
|
:title="$t('admin_dash.emoji.import_pack')"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<FAIcon icon="folder-open" />
|
||||||
class="button button-default emoji-panel-additional-actions"
|
{{ $t('admin_dash.emoji.import_pack_short') }}
|
||||||
:title="$t('admin_dash.emoji.import_pack')"
|
|
||||||
@click="$refs.additionalRemotePopover.showPopover"
|
|
||||||
>
|
|
||||||
<FAIcon icon="folder-open" />
|
|
||||||
{{ $t('admin_dash.emoji.import_pack_short') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
dd {
|
dd {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export default {
|
||||||
if (event === 'split') {
|
if (event === 'split') {
|
||||||
return [this.normalizedState[0], this.normalizedState[0]]
|
return [this.normalizedState[0], this.normalizedState[0]]
|
||||||
} else if (event === 'join') {
|
} else if (event === 'join') {
|
||||||
return [this.normalizedState[0]]
|
return this.normalizedState[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
</table>
|
</table>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="isSeparate"
|
:model-value="isSeparate"
|
||||||
@update:model-value="event => update({ event: event ? 'join' : 'split', eventType: 'toggleMode' })"
|
@update:model-value="event => update({ event: event ? 'split' : 'join', eventType: 'toggleMode' })"
|
||||||
>
|
>
|
||||||
{{ $t('admin_dash.rate_limit.separate') }}
|
{{ $t('admin_dash.rate_limit.separate') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
:title="$t('general.minimize')"
|
:title="$t('general.peek')"
|
||||||
@click="toggleMinimizeModal"
|
@click="toggleMinimizeModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings-admin-content settings_tab-switcher"
|
class="settings-admin-content settings_tab-switcher"
|
||||||
:side-tab-bar="true"
|
:side-tab-bar="true"
|
||||||
:scrollable-tabs
|
scrollable-tabs
|
||||||
:render-only-focused="true"
|
:render-only-focused="true"
|
||||||
:body-scroll-lock="bodyLock"
|
:body-scroll-lock="bodyLock"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<vertical-tab-switcher
|
<vertical-tab-switcher
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings_tab-switcher"
|
class="settings_tab-switcher"
|
||||||
:scrollable-tabs
|
scrollable-tabs
|
||||||
:body-scroll-lock="bodyLock"
|
:body-scroll-lock="bodyLock"
|
||||||
:hide-header="navHideHeader"
|
:hide-header="navHideHeader"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
class="mutes-and-blocks-tab"
|
class="mutes-and-blocks-tab"
|
||||||
:scrollable-tabs
|
scrollable-tabs
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="blocks"
|
class="blocks"
|
||||||
|
|
|
||||||
|
|
@ -73,46 +73,6 @@ library.add(
|
||||||
faPlay,
|
faPlay,
|
||||||
)
|
)
|
||||||
|
|
||||||
const camelCase = (name) => name.charAt(0).toUpperCase() + name.slice(1)
|
|
||||||
|
|
||||||
const controlledOrUncontrolledGetters = (list) =>
|
|
||||||
list.reduce((res, name) => {
|
|
||||||
const camelized = camelCase(name)
|
|
||||||
const toggle = `controlledToggle${camelized}`
|
|
||||||
const controlledName = `controlled${camelized}`
|
|
||||||
const uncontrolledName = `uncontrolled${camelized}`
|
|
||||||
res[name] = function () {
|
|
||||||
return (this.$data[toggle] !== undefined ||
|
|
||||||
this.$props[toggle] !== undefined) &&
|
|
||||||
this[toggle]
|
|
||||||
? this[controlledName]
|
|
||||||
: this[uncontrolledName]
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const controlledOrUncontrolledToggle = (obj, name) => {
|
|
||||||
const camelized = camelCase(name)
|
|
||||||
const toggle = `controlledToggle${camelized}`
|
|
||||||
const uncontrolledName = `uncontrolled${camelized}`
|
|
||||||
if (obj[toggle]) {
|
|
||||||
obj[toggle]()
|
|
||||||
} else {
|
|
||||||
obj[uncontrolledName] = !obj[uncontrolledName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const controlledOrUncontrolledSet = (obj, name, val) => {
|
|
||||||
const camelized = camelCase(name)
|
|
||||||
const set = `controlledSet${camelized}`
|
|
||||||
const uncontrolledName = `uncontrolled${camelized}`
|
|
||||||
if (obj[set]) {
|
|
||||||
obj[set](val)
|
|
||||||
} else {
|
|
||||||
obj[uncontrolledName] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -137,7 +97,6 @@ const Status = {
|
||||||
|
|
||||||
expandable: Boolean,
|
expandable: Boolean,
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
highlight: Boolean,
|
|
||||||
compact: Boolean,
|
compact: Boolean,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
noHeading: Boolean,
|
noHeading: Boolean,
|
||||||
|
|
@ -150,36 +109,23 @@ const Status = {
|
||||||
profileUserId: String,
|
profileUserId: String,
|
||||||
simpleTree: Boolean,
|
simpleTree: Boolean,
|
||||||
showOtherRepliesAsButton: Boolean,
|
showOtherRepliesAsButton: Boolean,
|
||||||
dive: Function,
|
canDive: Boolean,
|
||||||
ignoreMute: Boolean,
|
ignoreMute: Boolean,
|
||||||
|
|
||||||
controlledThreadDisplayStatus: String,
|
threadDisplayStatus: String,
|
||||||
controlledToggleThreadDisplay: Function,
|
|
||||||
controlledShowingTall: Boolean,
|
|
||||||
controlledToggleShowingTall: Function,
|
|
||||||
controlledExpandingSubject: Boolean,
|
|
||||||
controlledToggleExpandingSubject: Function,
|
|
||||||
controlledShowingLongSubject: Boolean,
|
|
||||||
controlledToggleShowingLongSubject: Function,
|
|
||||||
controlledReplying: Boolean,
|
|
||||||
controlledToggleReplying: Function,
|
|
||||||
controlledMediaPlaying: Boolean,
|
|
||||||
controlledSetMediaPlaying: Function,
|
|
||||||
},
|
},
|
||||||
emits: ['goto', 'toggleExpanded'],
|
emits: ['goto', 'dive', 'toggleExpanded', 'suspendableStateChange'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
uncontrolledReplying: false,
|
replying: false,
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
userExpanded: false,
|
||||||
uncontrolledMediaPlaying: [],
|
mediaPlaying: new Set(),
|
||||||
suspendable: true,
|
|
||||||
error: null,
|
error: null,
|
||||||
headTailLinks: null,
|
headTailLinks: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
|
||||||
showReasonMutedThread() {
|
showReasonMutedThread() {
|
||||||
return (
|
return (
|
||||||
(this.status.thread_muted ||
|
(this.status.thread_muted ||
|
||||||
|
|
@ -373,7 +319,7 @@ const Status = {
|
||||||
},
|
},
|
||||||
shouldNotMute() {
|
shouldNotMute() {
|
||||||
if (this.ignoreMute) return true
|
if (this.ignoreMute) return true
|
||||||
if (this.isFocused) return true
|
if (this.focused) return true
|
||||||
const { status } = this
|
const { status } = this
|
||||||
const { reblog } = status
|
const { reblog } = status
|
||||||
return (
|
return (
|
||||||
|
|
@ -410,16 +356,6 @@ const Status = {
|
||||||
this.muteFilterHits.some((x) => x.hide))
|
this.muteFilterHits.some((x) => x.hide))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isFocused() {
|
|
||||||
// retweet or root of an expanded conversation
|
|
||||||
if (this.focused) {
|
|
||||||
return true
|
|
||||||
} else if (!this.inConversation) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// use conversation highlight only when in conversation
|
|
||||||
return this.status.id === this.highlight
|
|
||||||
},
|
|
||||||
isReply() {
|
isReply() {
|
||||||
return !!(
|
return !!(
|
||||||
this.status.in_reply_to_status_id && this.status.in_reply_to_user_id
|
this.status.in_reply_to_status_id && this.status.in_reply_to_user_id
|
||||||
|
|
@ -468,7 +404,7 @@ const Status = {
|
||||||
shouldDisplayFavsAndRepeats() {
|
shouldDisplayFavsAndRepeats() {
|
||||||
return (
|
return (
|
||||||
!this.hidePostStats &&
|
!this.hidePostStats &&
|
||||||
this.isFocused &&
|
this.focused &&
|
||||||
(this.combinedFavsAndRepeatsUsers.length > 0 ||
|
(this.combinedFavsAndRepeatsUsers.length > 0 ||
|
||||||
this.statusFromGlobalRepository.quotes_count)
|
this.statusFromGlobalRepository.quotes_count)
|
||||||
)
|
)
|
||||||
|
|
@ -489,13 +425,13 @@ const Status = {
|
||||||
return useMergedConfigStore().mergedConfig
|
return useMergedConfigStore().mergedConfig
|
||||||
},
|
},
|
||||||
isSuspendable() {
|
isSuspendable() {
|
||||||
return !this.replying && this.mediaPlaying.length === 0
|
return !this.replying && this.mediaPlaying.size === 0
|
||||||
},
|
},
|
||||||
inThreadForest() {
|
inThreadForest() {
|
||||||
return !!this.controlledThreadDisplayStatus
|
return !!this.threadDisplayStatus
|
||||||
},
|
},
|
||||||
threadShowing() {
|
threadShowing() {
|
||||||
return this.controlledThreadDisplayStatus === 'showing'
|
return this.threadDisplayStatus === 'showing'
|
||||||
},
|
},
|
||||||
visibilityLocalized() {
|
visibilityLocalized() {
|
||||||
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
|
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
|
||||||
|
|
@ -566,15 +502,17 @@ const Status = {
|
||||||
clearError() {
|
clearError() {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
},
|
},
|
||||||
toggleReplying() {
|
toggleReplyForm() {
|
||||||
if (this.replying) {
|
if (this.replying) {
|
||||||
|
// This emits 'close-accepted' if successful
|
||||||
|
// which in turn callse closeReply()
|
||||||
this.$refs.postStatusForm.requestClose()
|
this.$refs.postStatusForm.requestClose()
|
||||||
} else {
|
} else {
|
||||||
this.doToggleReplying()
|
this.replying = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doToggleReplying() {
|
closeReplyForm() {
|
||||||
controlledOrUncontrolledToggle(this, 'replying')
|
this.replying = false
|
||||||
},
|
},
|
||||||
gotoOriginal(id) {
|
gotoOriginal(id) {
|
||||||
if (this.inConversation) {
|
if (this.inConversation) {
|
||||||
|
|
@ -598,18 +536,10 @@ const Status = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
addMediaPlaying(id) {
|
addMediaPlaying(id) {
|
||||||
controlledOrUncontrolledSet(
|
this.mediaPlaying.add(id)
|
||||||
this,
|
|
||||||
'mediaPlaying',
|
|
||||||
this.mediaPlaying.concat(id),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
removeMediaPlaying(id) {
|
removeMediaPlaying(id) {
|
||||||
controlledOrUncontrolledSet(
|
this.mediaPlaying.delete(id)
|
||||||
this,
|
|
||||||
'mediaPlaying',
|
|
||||||
this.mediaPlaying.filter((mediaId) => mediaId !== id),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
setHeadTailLinks(headTailLinks) {
|
setHeadTailLinks(headTailLinks) {
|
||||||
this.headTailLinks = headTailLinks
|
this.headTailLinks = headTailLinks
|
||||||
|
|
@ -617,9 +547,9 @@ const Status = {
|
||||||
toggleThreadDisplay() {
|
toggleThreadDisplay() {
|
||||||
this.controlledToggleThreadDisplay()
|
this.controlledToggleThreadDisplay()
|
||||||
},
|
},
|
||||||
scrollIfHighlighted(highlightId) {
|
scrollIfFocused(focusedId) {
|
||||||
if (this.$el.getBoundingClientRect == null) return
|
if (this.$el.getBoundingClientRect == null) return
|
||||||
const id = highlightId
|
const id = focusedId
|
||||||
if (this.status.id === id) {
|
if (this.status.id === id) {
|
||||||
const rect = this.$el.getBoundingClientRect()
|
const rect = this.$el.getBoundingClientRect()
|
||||||
if (rect.top < 100) {
|
if (rect.top < 100) {
|
||||||
|
|
@ -636,13 +566,13 @@ const Status = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
highlight: function (id) {
|
focused: function (id) {
|
||||||
this.scrollIfHighlighted(id)
|
this.scrollIfFocused(id)
|
||||||
},
|
},
|
||||||
'status.repeat_num': function (num) {
|
'status.repeat_num': function (num) {
|
||||||
// refetch repeats when repeat_num is changed in any way
|
// refetch repeats when repeat_num is changed in any way
|
||||||
if (
|
if (
|
||||||
this.isFocused &&
|
this.focused &&
|
||||||
this.statusFromGlobalRepository.rebloggedBy &&
|
this.statusFromGlobalRepository.rebloggedBy &&
|
||||||
this.statusFromGlobalRepository.rebloggedBy.length !== num
|
this.statusFromGlobalRepository.rebloggedBy.length !== num
|
||||||
) {
|
) {
|
||||||
|
|
@ -652,15 +582,15 @@ const Status = {
|
||||||
'status.fave_num': function (num) {
|
'status.fave_num': function (num) {
|
||||||
// refetch favs when fave_num is changed in any way
|
// refetch favs when fave_num is changed in any way
|
||||||
if (
|
if (
|
||||||
this.isFocused &&
|
this.focused &&
|
||||||
this.statusFromGlobalRepository.favoritedBy &&
|
this.statusFromGlobalRepository.favoritedBy &&
|
||||||
this.statusFromGlobalRepository.favoritedBy.length !== num
|
this.statusFromGlobalRepository.favoritedBy.length !== num
|
||||||
) {
|
) {
|
||||||
this.$store.dispatch('fetchFavs', this.status.id)
|
this.$store.dispatch('fetchFavs', this.status.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSuspendable: function (val) {
|
isSuspendable: function (suspend) {
|
||||||
this.suspendable = val
|
this.$emit('suspendableStateChange', { id: this.statusoid.id, suspend })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
v-if="!hideStatus"
|
v-if="!hideStatus"
|
||||||
ref="root"
|
ref="root"
|
||||||
class="Status"
|
class="Status"
|
||||||
:class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
|
:class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="error"
|
||||||
|
|
@ -236,10 +236,10 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="dive && !simpleTree"
|
v-if="canDive && !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="dive"
|
@click.prevent="$emit('dive')"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
|
|
@ -409,15 +409,8 @@
|
||||||
<StatusContent
|
<StatusContent
|
||||||
ref="content"
|
ref="content"
|
||||||
:status="status"
|
:status="status"
|
||||||
:no-heading="noHeading"
|
:focused="focused"
|
||||||
:highlight="highlight"
|
:in-conversation="inConversation"
|
||||||
:focused="isFocused"
|
|
||||||
:controlled-showing-tall="controlledShowingTall"
|
|
||||||
:controlled-expanding-subject="controlledExpandingSubject"
|
|
||||||
:controlled-showing-long-subject="controlledShowingLongSubject"
|
|
||||||
:controlled-toggle-showing-tall="controlledToggleShowingTall"
|
|
||||||
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
|
|
||||||
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
|
||||||
@mediaplay="addMediaPlaying($event)"
|
@mediaplay="addMediaPlaying($event)"
|
||||||
@mediapause="removeMediaPlaying($event)"
|
@mediapause="removeMediaPlaying($event)"
|
||||||
@parse-ready="setHeadTailLinks"
|
@parse-ready="setHeadTailLinks"
|
||||||
|
|
@ -438,7 +431,7 @@
|
||||||
v-if="showOtherRepliesAsButton && replies.length > 1"
|
v-if="showOtherRepliesAsButton && replies.length > 1"
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
:title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)"
|
:title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)"
|
||||||
@click.prevent="dive"
|
@click.prevent="$emit('dive')"
|
||||||
>
|
>
|
||||||
{{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }}
|
{{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -513,7 +506,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<EmojiReactions
|
<EmojiReactions
|
||||||
v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
|
v-if="(mergedConfig.emojiReactionsOnTimeline || focused) && (!noHeading && !isPreview)"
|
||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -521,7 +514,7 @@
|
||||||
v-if="!noHeading && !isPreview"
|
v-if="!noHeading && !isPreview"
|
||||||
:status="status"
|
:status="status"
|
||||||
:replying="replying"
|
:replying="replying"
|
||||||
@toggle-replying="toggleReplying"
|
@toggle-replying="toggleReplyForm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -555,9 +548,9 @@
|
||||||
:replied-user="status.user"
|
:replied-user="status.user"
|
||||||
:copy-message-scope="status.visibility"
|
:copy-message-scope="status.visibility"
|
||||||
:subject="replySubject"
|
:subject="replySubject"
|
||||||
@posted="doToggleReplying"
|
@posted="closeReplyForm"
|
||||||
@draft-done="doToggleReplying"
|
@draft-done="closeReplyForm"
|
||||||
@can-close="doToggleReplying"
|
@close-accepted="closeReplyForm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
|
||||||
|
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
|
|
@ -72,12 +71,7 @@ export default {
|
||||||
'outerClose',
|
'outerClose',
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
StatusBookmarkFolderMenu: defineAsyncComponent(
|
StatusBookmarkFolderMenu,
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
Popover,
|
Popover,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,32 +15,47 @@ library.add(faFile, faMusic, faImage, faLink, faPollH)
|
||||||
|
|
||||||
const StatusBody = {
|
const StatusBody = {
|
||||||
name: 'StatusBody',
|
name: 'StatusBody',
|
||||||
props: [
|
props: {
|
||||||
'compact',
|
status: {
|
||||||
'collapse', // replaces newlines with spaces
|
// Main thing
|
||||||
'status',
|
type: Object,
|
||||||
'focused',
|
required: true,
|
||||||
'noHeading',
|
},
|
||||||
'fullContent',
|
compact: {
|
||||||
'singleLine',
|
// Resizes emoji and minimizes vertical space used
|
||||||
'showingTall',
|
// Primarily used for showing status in react notifications
|
||||||
'expandingSubject',
|
type: Boolean,
|
||||||
'showingLongSubject',
|
default: false,
|
||||||
'toggleShowingTall',
|
},
|
||||||
'toggleExpandingSubject',
|
collapse: {
|
||||||
'toggleShowingLongSubject',
|
// replaces newlines with spaces
|
||||||
],
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
singleLine: {
|
||||||
|
// Show entire thing (subject and content) in a single line
|
||||||
|
// Primarily used in chats
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
inConversation: {
|
||||||
|
// Is status rendered within open conversation?
|
||||||
|
// Used to automatically expand subjects (if collapsed)
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
postLength: this.status.text.length,
|
postLength: this.status.text.length,
|
||||||
parseReadyDone: false,
|
parseReadyDone: false,
|
||||||
|
showingTall: false,
|
||||||
|
showingLongSubject: false,
|
||||||
|
expandingSubject: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['parseReady'],
|
emits: ['parseReady'],
|
||||||
computed: {
|
computed: {
|
||||||
localCollapseSubjectDefault() {
|
|
||||||
return this.mergedConfig.collapseMessageWithSubject
|
|
||||||
},
|
|
||||||
allowNonSquareEmoji() {
|
allowNonSquareEmoji() {
|
||||||
return this.mergedConfig.nonSquareEmoji
|
return this.mergedConfig.nonSquareEmoji
|
||||||
},
|
},
|
||||||
|
|
@ -51,32 +66,35 @@ const StatusBody = {
|
||||||
//
|
//
|
||||||
// Using max-height + overflow: auto for status components resulted in false positives
|
// Using max-height + overflow: auto for status components resulted in false positives
|
||||||
// very often with japanese characters, and it was very annoying.
|
// very often with japanese characters, and it was very annoying.
|
||||||
tallStatus() {
|
hasLongSubject() {
|
||||||
|
return this.status.summary.length > 240
|
||||||
|
},
|
||||||
|
hasSubject() {
|
||||||
|
return !!this.status.summary
|
||||||
|
},
|
||||||
|
// When a status has a subject and is also tall, we should only have one show more/less
|
||||||
|
// button. If the default is to collapse statuses with subjects, we just treat it like
|
||||||
|
// a status with a subject; otherwise, we just treat it like a tall status.
|
||||||
|
mightHideBecauseSubject() {
|
||||||
|
return (
|
||||||
|
!this.inConversation &&
|
||||||
|
this.hasSubject &&
|
||||||
|
this.mergedConfig.collapseMessageWithSubject
|
||||||
|
)
|
||||||
|
},
|
||||||
|
mightHideBecauseTall() {
|
||||||
if (this.singleLine || this.compact) return false
|
if (this.singleLine || this.compact) return false
|
||||||
const lengthScore =
|
const lengthScore =
|
||||||
this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
|
this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
|
||||||
return lengthScore > 20
|
return lengthScore > 20
|
||||||
},
|
},
|
||||||
longSubject() {
|
|
||||||
return this.status.summary.length > 240
|
|
||||||
},
|
|
||||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
|
||||||
mightHideBecauseSubject() {
|
|
||||||
return !!this.status.summary && this.localCollapseSubjectDefault
|
|
||||||
},
|
|
||||||
mightHideBecauseTall() {
|
|
||||||
return (
|
|
||||||
this.tallStatus &&
|
|
||||||
!(this.status.summary && this.localCollapseSubjectDefault)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hideSubjectStatus() {
|
hideSubjectStatus() {
|
||||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||||
},
|
},
|
||||||
hideTallStatus() {
|
hideTallStatus() {
|
||||||
return this.mightHideBecauseTall && !this.showingTall
|
return this.mightHideBecauseTall && !this.showingTall
|
||||||
},
|
},
|
||||||
shouldShowToggle() {
|
shouldShowExpandToggle() {
|
||||||
return this.mightHideBecauseSubject || this.mightHideBecauseTall
|
return this.mightHideBecauseSubject || this.mightHideBecauseTall
|
||||||
},
|
},
|
||||||
toggleButtonClasses() {
|
toggleButtonClasses() {
|
||||||
|
|
@ -97,6 +115,11 @@ const StatusBody = {
|
||||||
: this.$t('general.show_more')
|
: this.$t('general.show_more')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
shouldHide() {
|
||||||
|
return (
|
||||||
|
!this.showingMore && this.mightHideBecauseSubject && this.hasSubject
|
||||||
|
)
|
||||||
|
},
|
||||||
showingMore() {
|
showingMore() {
|
||||||
return (
|
return (
|
||||||
(this.mightHideBecauseTall && this.showingTall) ||
|
(this.mightHideBecauseTall && this.showingTall) ||
|
||||||
|
|
@ -147,9 +170,9 @@ const StatusBody = {
|
||||||
},
|
},
|
||||||
toggleShowMore() {
|
toggleShowMore() {
|
||||||
if (this.mightHideBecauseTall) {
|
if (this.mightHideBecauseTall) {
|
||||||
this.toggleShowingTall()
|
this.showingTall = !this.showingTall
|
||||||
} else if (this.mightHideBecauseSubject) {
|
} else if (this.mightHideBecauseSubject) {
|
||||||
this.toggleExpandingSubject()
|
this.expandingSubject = !this.expandingSubject
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateTagLink(tag) {
|
generateTagLink(tag) {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-wrapper {
|
.text-wrapper {
|
||||||
|
position: relative;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -60,10 +61,12 @@
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
|
|
||||||
&.-tall-status {
|
&.-tall-status {
|
||||||
position: relative;
|
|
||||||
height: 16em;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
&:not(.-hidden) {
|
||||||
|
height: 16em;
|
||||||
|
}
|
||||||
|
|
||||||
.media-body {
|
.media-body {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
mask:
|
mask:
|
||||||
|
|
@ -98,6 +101,7 @@
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: 0.1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-unhider {
|
.status-unhider {
|
||||||
|
|
@ -107,17 +111,17 @@
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tall-status-hider {
|
|
||||||
position: absolute;
|
|
||||||
height: 5em;
|
|
||||||
margin-top: 10em;
|
|
||||||
line-height: 8em;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tall-subject-hider {
|
.tall-subject-hider {
|
||||||
// position: absolute;
|
// position: absolute;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
&:not(.cw-status-hider) {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 10em;
|
||||||
|
height: 5em;
|
||||||
|
line-height: 8em;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .status-unhider,
|
& .status-unhider,
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
>
|
>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div
|
<div
|
||||||
v-if="status.summary_raw_html"
|
v-if="hasSubject"
|
||||||
class="summary-wrapper"
|
class="summary-wrapper"
|
||||||
:class="{ '-tall': (longSubject && !showingLongSubject) }"
|
:class="{ '-tall': (hasLongSubject && !showingLongSubject) }"
|
||||||
>
|
>
|
||||||
<RichContent
|
<RichContent
|
||||||
class="media-body summary"
|
class="media-body summary"
|
||||||
|
|
@ -18,14 +18,14 @@
|
||||||
:allow-non-square-emoji="allowNonSquareEmoji"
|
:allow-non-square-emoji="allowNonSquareEmoji"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-show="longSubject && showingLongSubject"
|
v-show="hasLongSubject && showingLongSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="toggleShowingLongSubject"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
{{ $t("status.hide_full_subject") }}
|
{{ $t("status.hide_full_subject") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-show="longSubject && !showingLongSubject"
|
v-show="hasLongSubject && !showingLongSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="toggleShowingLongSubject"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
|
|
@ -34,10 +34,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-wrapper"
|
class="text-wrapper"
|
||||||
:class="{'-tall-status': hideTallStatus, '-expanded': showingMore}"
|
:class="{'-tall-status': hideTallStatus, '-hidden': shouldHide, '-expanded': showingMore}"
|
||||||
>
|
>
|
||||||
<RichContent
|
<RichContent
|
||||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
|
v-if="!(singleLine && hasSubject) && !shouldHide"
|
||||||
:class="{ '-single-line': singleLine }"
|
:class="{ '-single-line': singleLine }"
|
||||||
class="text media-body"
|
class="text media-body"
|
||||||
:html="status.raw_html"
|
:html="status.raw_html"
|
||||||
|
|
@ -52,12 +52,11 @@
|
||||||
@parse-ready="onParseReady"
|
@parse-ready="onParseReady"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-show="shouldShowToggle"
|
v-show="shouldShowExpandToggle"
|
||||||
:class="toggleButtonClasses"
|
:class="toggleButtonClasses"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn button-default toggle-button"
|
class="btn button-default toggle-button"
|
||||||
:class="{ '-focused': focused }"
|
|
||||||
:aria-expanded="showingMore"
|
:aria-expanded="showingMore"
|
||||||
@click.prevent="toggleShowMore"
|
@click.prevent="toggleShowMore"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -22,69 +22,40 @@ import {
|
||||||
|
|
||||||
library.add(faCircleNotch, faFile, faMusic, faImage, faLink, faPollH)
|
library.add(faCircleNotch, faFile, faMusic, faImage, faLink, faPollH)
|
||||||
|
|
||||||
const camelCase = (name) => name.charAt(0).toUpperCase() + name.slice(1)
|
|
||||||
|
|
||||||
const controlledOrUncontrolledGetters = (list) =>
|
|
||||||
list.reduce((res, name) => {
|
|
||||||
const camelized = camelCase(name)
|
|
||||||
const toggle = `controlledToggle${camelized}`
|
|
||||||
const controlledName = `controlled${camelized}`
|
|
||||||
const uncontrolledName = `uncontrolled${camelized}`
|
|
||||||
res[name] = function () {
|
|
||||||
return (this.$data[toggle] !== undefined ||
|
|
||||||
this.$props[toggle] !== undefined) &&
|
|
||||||
this[toggle]
|
|
||||||
? this[controlledName]
|
|
||||||
: this[uncontrolledName]
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const controlledOrUncontrolledToggle = (obj, name) => {
|
|
||||||
const camelized = camelCase(name)
|
|
||||||
const toggle = `controlledToggle${camelized}`
|
|
||||||
const uncontrolledName = `uncontrolled${camelized}`
|
|
||||||
if (obj[toggle]) {
|
|
||||||
obj[toggle]()
|
|
||||||
} else {
|
|
||||||
obj[uncontrolledName] = !obj[uncontrolledName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatusContent = {
|
const StatusContent = {
|
||||||
name: 'StatusContent',
|
name: 'StatusContent',
|
||||||
props: [
|
props: {
|
||||||
'status',
|
status: {
|
||||||
'compact',
|
// Main thing
|
||||||
'collapse',
|
type: Object,
|
||||||
'focused',
|
required: true,
|
||||||
'noHeading',
|
},
|
||||||
'fullContent',
|
compact: {
|
||||||
'singleLine',
|
// Resizes emoji and minimizes vertical space used
|
||||||
'controlledShowingTall',
|
// Primarily used for showing status in react notifications
|
||||||
'controlledExpandingSubject',
|
type: Boolean,
|
||||||
'controlledToggleShowingTall',
|
default: false,
|
||||||
'controlledToggleExpandingSubject',
|
},
|
||||||
'controlledShowingLongSubject',
|
collapse: {
|
||||||
'controlledToggleShowingLongSubject',
|
// replaces newlines with spaces
|
||||||
],
|
type: Boolean,
|
||||||
emits: ['parseReady', 'mediaplay', 'mediapause'],
|
default: false,
|
||||||
data() {
|
},
|
||||||
return {
|
singleLine: {
|
||||||
uncontrolledShowingTall:
|
// Show entire thing (subject and content) in a single line
|
||||||
this.fullContent || (this.inConversation && this.focused),
|
// Primarily used in chats
|
||||||
uncontrolledShowingLongSubject: false,
|
type: Boolean,
|
||||||
// not as computed because it sets the initial state which will be changed later
|
default: false,
|
||||||
uncontrolledExpandingSubject:
|
},
|
||||||
!useMergedConfigStore().mergedConfig.collapseMessageWithSubject,
|
inConversation: {
|
||||||
}
|
// Whether status content is being shown in an (open) conversation
|
||||||
|
// Used to control whether to display attachments or not
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['parseReady', 'mediaplay', 'mediapause'],
|
||||||
computed: {
|
computed: {
|
||||||
...controlledOrUncontrolledGetters([
|
|
||||||
'showingTall',
|
|
||||||
'expandingSubject',
|
|
||||||
'showingLongSubject',
|
|
||||||
]),
|
|
||||||
statusCard() {
|
statusCard() {
|
||||||
if (!this.status.card) return null
|
if (!this.status.card) return null
|
||||||
return this.status.card.url === this.status.quote_url
|
return this.status.card.url === this.status.quote_url
|
||||||
|
|
@ -93,22 +64,11 @@ const StatusContent = {
|
||||||
},
|
},
|
||||||
hideAttachments() {
|
hideAttachments() {
|
||||||
return (
|
return (
|
||||||
(this.mergedConfig.hideAttachments && !this.inConversation) ||
|
!this.fullContent &&
|
||||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||||
|
(this.mergedConfig.hideAttachmentsInConv && this.inConversation))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
nsfwClickthrough() {
|
|
||||||
if (!this.status.nsfw) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
localCollapseSubjectDefault() {
|
|
||||||
return this.mergedConfig.collapseMessageWithSubject
|
|
||||||
},
|
|
||||||
attachmentSize() {
|
attachmentSize() {
|
||||||
if (this.compact) {
|
if (this.compact) {
|
||||||
return 'small'
|
return 'small'
|
||||||
|
|
@ -137,15 +97,6 @@ const StatusContent = {
|
||||||
StatusBody,
|
StatusBody,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleShowingTall() {
|
|
||||||
controlledOrUncontrolledToggle(this, 'showingTall')
|
|
||||||
},
|
|
||||||
toggleExpandingSubject() {
|
|
||||||
controlledOrUncontrolledToggle(this, 'expandingSubject')
|
|
||||||
},
|
|
||||||
toggleShowingLongSubject() {
|
|
||||||
controlledOrUncontrolledToggle(this, 'showingLongSubject')
|
|
||||||
},
|
|
||||||
setMedia() {
|
setMedia() {
|
||||||
const attachments =
|
const attachments =
|
||||||
this.attachmentSize === 'hide'
|
this.attachmentSize === 'hide'
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,8 @@
|
||||||
:status="status"
|
:status="status"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:single-line="singleLine"
|
:single-line="singleLine"
|
||||||
:showing-tall="showingTall"
|
|
||||||
:expanding-subject="expandingSubject"
|
|
||||||
:showing-long-subject="showingLongSubject"
|
|
||||||
:toggle-showing-tall="toggleShowingTall"
|
|
||||||
:toggle-expanding-subject="toggleExpandingSubject"
|
|
||||||
:toggle-showing-long-subject="toggleShowingLongSubject"
|
|
||||||
:collapse="collapse"
|
:collapse="collapse"
|
||||||
|
:in-conversation="inConversation"
|
||||||
@parse-ready="$emit('parseReady', $event)"
|
@parse-ready="$emit('parseReady', $event)"
|
||||||
>
|
>
|
||||||
<div v-if="status.poll && status.poll.options && !compact">
|
<div v-if="status.poll && status.poll.options && !compact">
|
||||||
|
|
@ -38,22 +33,22 @@
|
||||||
v-if="status.attachments.length !== 0"
|
v-if="status.attachments.length !== 0"
|
||||||
class="attachments media-body"
|
class="attachments media-body"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:nsfw="nsfwClickthrough"
|
:nsfw="status.nsfw"
|
||||||
:attachments="status.attachments"
|
:attachments="status.attachments"
|
||||||
:limit="compact ? 1 : 0"
|
:limit="compact ? 1 : 0"
|
||||||
:size="attachmentSize"
|
:size="attachmentSize"
|
||||||
@play="$emit('mediaplay', attachment.id)"
|
@play="$emit('mediaplay')"
|
||||||
@pause="$emit('mediapause', attachment.id)"
|
@pause="$emit('mediapause')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="statusCard && !noHeading && !compact"
|
v-if="statusCard && !compact"
|
||||||
class="link-preview media-body"
|
class="link-preview media-body"
|
||||||
>
|
>
|
||||||
<link-preview
|
<LinkPreview
|
||||||
:card="status.card"
|
:card="status.card"
|
||||||
:size="attachmentSize"
|
:size="attachmentSize"
|
||||||
:nsfw="nsfwClickthrough"
|
:nsfw="status.nsfw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StatusBody>
|
</StatusBody>
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,7 @@ export default {
|
||||||
shortcode: this.shortcode,
|
shortcode: this.shortcode,
|
||||||
filename: '',
|
filename: '',
|
||||||
})
|
})
|
||||||
.then((resp) => resp.json())
|
.then(({ data: resp }) => {
|
||||||
.then((resp) => {
|
|
||||||
if (resp.error !== undefined) {
|
|
||||||
this.displayError(resp.error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
useInterfaceStore().pushGlobalNotice({
|
useInterfaceStore().pushGlobalNotice({
|
||||||
messageKey: 'admin_dash.emoji.copied_successfully',
|
messageKey: 'admin_dash.emoji.copied_successfully',
|
||||||
messageArgs: [this.shortcode, this.packName],
|
messageArgs: [this.shortcode, this.packName],
|
||||||
|
|
@ -60,6 +55,10 @@ export default {
|
||||||
this.$refs.emojiPopover.hidePopover()
|
this.$refs.emojiPopover.hidePopover()
|
||||||
this.packName = ''
|
this.packName = ''
|
||||||
})
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.displayError(e)
|
||||||
|
return
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchEmojiPacksIfAdmin() {
|
fetchEmojiPacksIfAdmin() {
|
||||||
|
|
|
||||||
|
|
@ -19,37 +19,19 @@ const ThreadTree = {
|
||||||
pinnedStatusIdsObject: Object,
|
pinnedStatusIdsObject: Object,
|
||||||
profileUserId: String,
|
profileUserId: String,
|
||||||
|
|
||||||
isFocusedFunction: Function,
|
focused: String,
|
||||||
highlight: String,
|
|
||||||
getReplies: Function,
|
getReplies: Function,
|
||||||
setHighlight: Function,
|
|
||||||
toggleExpanded: Function,
|
toggleExpanded: Function,
|
||||||
|
|
||||||
simple: Boolean,
|
simple: Boolean,
|
||||||
// to control display of the whole thread forest
|
canDive: Boolean,
|
||||||
toggleThreadDisplay: Function,
|
|
||||||
threadDisplayStatus: Object,
|
threadDisplayStatus: Object,
|
||||||
showThreadRecursively: Function,
|
showThreadRecursively: Function,
|
||||||
totalReplyCount: Object,
|
totalReplyCount: Object,
|
||||||
totalReplyDepth: Object,
|
totalReplyDepth: Object,
|
||||||
statusContentProperties: Object,
|
|
||||||
setStatusContentProperty: Function,
|
|
||||||
toggleStatusContentProperty: Function,
|
|
||||||
dive: Function,
|
|
||||||
},
|
},
|
||||||
|
emits: ['suspendableStateChange', 'goto', 'dive'],
|
||||||
computed: {
|
computed: {
|
||||||
suspendable() {
|
|
||||||
const selfSuspendable = this.$refs.statusComponent
|
|
||||||
? this.$refs.statusComponent.suspendable
|
|
||||||
: true
|
|
||||||
if (this.$refs.childComponent) {
|
|
||||||
return (
|
|
||||||
selfSuspendable &&
|
|
||||||
this.$refs.childComponent.every((s) => s.suspendable)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return selfSuspendable
|
|
||||||
},
|
|
||||||
reverseLookupTable() {
|
reverseLookupTable() {
|
||||||
return this.conversation.reduce(
|
return this.conversation.reduce(
|
||||||
(table, status, index) => {
|
(table, status, index) => {
|
||||||
|
|
@ -69,29 +51,11 @@ const ThreadTree = {
|
||||||
threadShowing() {
|
threadShowing() {
|
||||||
return this.threadDisplayStatus[this.status.id] === 'showing'
|
return this.threadDisplayStatus[this.status.id] === 'showing'
|
||||||
},
|
},
|
||||||
currentProp() {
|
|
||||||
return this.statusContentProperties[this.status.id]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
statusById(id) {
|
statusById(id) {
|
||||||
return this.conversation[this.reverseLookupTable[id]]
|
return this.conversation[this.reverseLookupTable[id]]
|
||||||
},
|
},
|
||||||
collapseThread() {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
showThread() {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
showAllSubthreads() {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
toggleCurrentProp(name) {
|
|
||||||
this.toggleStatusContentProperty(this.status.id, name)
|
|
||||||
},
|
|
||||||
setCurrentProp(name) {
|
|
||||||
this.setStatusContentProperty(this.status.id, name)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<article class="thread-tree">
|
<article class="thread-tree">
|
||||||
<status
|
<Status
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
:inline-expanded="collapsable && isExpanded"
|
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
:focused="isFocusedFunction(status.id)"
|
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:highlight="highlight"
|
:focused="focused === status.id || focused === status.retweeted_status?.id"
|
||||||
:replies="getReplies(status.id)"
|
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
class="conversation-status conversation-status-treeview status-fadein panel-body"
|
class="conversation-status conversation-status-treeview status-fadein panel-body"
|
||||||
|
|
||||||
:simple-tree="simple"
|
:simple-tree="simple"
|
||||||
:controlled-thread-display-status="threadDisplayStatus[status.id]"
|
:thread-display-status="threadDisplayStatus[status.id]"
|
||||||
:controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
|
:can-dive="canDive"
|
||||||
|
|
||||||
:controlled-showing-tall="currentProp.showingTall"
|
@dive="$emit('dive', status.id)"
|
||||||
:controlled-expanding-subject="currentProp.expandingSubject"
|
@goto="$emit('goto', status.id)"
|
||||||
:controlled-showing-long-subject="currentProp.showingLongSubject"
|
|
||||||
:controlled-replying="currentProp.replying"
|
|
||||||
:controlled-media-playing="currentProp.mediaPlaying"
|
|
||||||
:controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')"
|
|
||||||
:controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
|
|
||||||
:controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
|
|
||||||
:controlled-toggle-replying="() => toggleCurrentProp('replying')"
|
|
||||||
:controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)"
|
|
||||||
:dive="dive ? () => dive(status.id) : undefined"
|
|
||||||
|
|
||||||
@goto="setHighlight"
|
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
|
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="currentReplies.length && threadShowing"
|
v-if="currentReplies.length > 0 && threadShowing"
|
||||||
class="thread-tree-replies"
|
class="thread-tree-replies"
|
||||||
>
|
>
|
||||||
<thread-tree
|
<ThreadTree
|
||||||
v-for="replyStatus in currentReplies"
|
v-for="replyStatus in currentReplies"
|
||||||
:key="replyStatus.id"
|
:key="replyStatus.id"
|
||||||
ref="childComponent"
|
ref="childComponent"
|
||||||
|
|
@ -52,22 +41,20 @@
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:is-focused-function="isFocusedFunction"
|
|
||||||
:get-replies="getReplies"
|
:get-replies="getReplies"
|
||||||
:highlight="highlight"
|
:focused="focused"
|
||||||
:set-highlight="setHighlight"
|
|
||||||
:toggle-expanded="toggleExpanded"
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
:simple="simple"
|
:simple="simple"
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
:show-thread-recursively="showThreadRecursively"
|
||||||
:total-reply-count="totalReplyCount"
|
:total-reply-count="totalReplyCount"
|
||||||
:total-reply-depth="totalReplyDepth"
|
:total-reply-depth="totalReplyDepth"
|
||||||
:status-content-properties="statusContentProperties"
|
|
||||||
:set-status-content-property="setStatusContentProperty"
|
:can-dive="canDive"
|
||||||
:toggle-status-content-property="toggleStatusContentProperty"
|
@goto="(e) => $emit('goto', e)"
|
||||||
:dive="dive"
|
@dive="(e) => $emit('dive', e)"
|
||||||
|
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -80,7 +67,7 @@
|
||||||
tag="button"
|
tag="button"
|
||||||
keypath="status.thread_follow_with_icon"
|
keypath="status.thread_follow_with_icon"
|
||||||
class="button-unstyled -link thread-tree-show-replies-button"
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
@click.prevent="dive(status.id)"
|
@click.prevent="$emit('dive', status.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
||||||
|
|
@ -71,27 +71,27 @@
|
||||||
class="timeline"
|
class="timeline"
|
||||||
role="feed"
|
role="feed"
|
||||||
>
|
>
|
||||||
<conversation
|
<Conversation
|
||||||
v-for="statusId in filteredPinnedStatusIds"
|
v-for="statusId in filteredPinnedStatusIds"
|
||||||
:key="statusId + '-pinned'"
|
:key="statusId + '-pinned'"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
class="status-fadein"
|
class="status-fadein"
|
||||||
:status-id="statusId"
|
:status-id="statusId"
|
||||||
:collapsable="true"
|
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="userId"
|
:profile-user-id="userId"
|
||||||
|
collapsable
|
||||||
/>
|
/>
|
||||||
<conversation
|
<Conversation
|
||||||
v-for="status in filteredVisibleStatuses"
|
v-for="status in filteredVisibleStatuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
class="status-fadein"
|
class="status-fadein"
|
||||||
:status-id="status.id"
|
:status-id="status.id"
|
||||||
:collapsable="true"
|
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="userId"
|
:profile-user-id="userId"
|
||||||
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
|
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
|
||||||
|
collapsable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||||
|
import FollowButton from 'src/components/follow_button/follow_button.vue'
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
@ -138,9 +139,7 @@ export default {
|
||||||
() => import('src/components/account_actions/account_actions.vue'),
|
() => import('src/components/account_actions/account_actions.vue'),
|
||||||
),
|
),
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
FollowButton: defineAsyncComponent(
|
FollowButton,
|
||||||
() => import('src/components/follow_button/follow_button.vue'),
|
|
||||||
),
|
|
||||||
Select,
|
Select,
|
||||||
UserLink,
|
UserLink,
|
||||||
UserNote: defineAsyncComponent(
|
UserNote: defineAsyncComponent(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import AuthForm from 'src/components/auth_form/auth_form.js'
|
||||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
import UserCard from 'src/components/user_card/user_card.vue'
|
import UserCard from 'src/components/user_card/user_card.vue'
|
||||||
|
|
||||||
|
|
@ -12,11 +12,9 @@ const UserPanel = {
|
||||||
...mapState({ user: (state) => state.users.currentUser }),
|
...mapState({ user: (state) => state.users.currentUser }),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
AuthForm: defineAsyncComponent(
|
|
||||||
() => import('src/components/auth_form/auth_form.js'),
|
|
||||||
),
|
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
UserCard,
|
UserCard,
|
||||||
|
AuthForm,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
/>
|
/>
|
||||||
<PostStatusForm />
|
<PostStatusForm />
|
||||||
</div>
|
</div>
|
||||||
<auth-form
|
<AuthForm
|
||||||
v-else
|
v-else
|
||||||
key="user-panel"
|
key="user-panel"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,11 @@
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"generic_error": "An error occured",
|
"generic_error": "An error occured",
|
||||||
"generic_error_message": "An error occured: {0}",
|
"generic_error_message": "An error occured: {0}",
|
||||||
|
"generic_error_details": "Technical info:",
|
||||||
"error_retry": "Please try again",
|
"error_retry": "Please try again",
|
||||||
|
"refresh_required": "Refresh required",
|
||||||
|
"refresh_required_content": "Failed to load UI code. Most likely frontend was updated on server, you'll need to refresh the page.",
|
||||||
|
"refresh_required_refresh": "Refresh page",
|
||||||
"retry": "Try again",
|
"retry": "Try again",
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
|
|
@ -1476,10 +1480,36 @@
|
||||||
"description": "Number of retries for making a connection CONFIRM"
|
"description": "Number of retries for making a connection CONFIRM"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
":hackney_pools": {
|
||||||
|
":rich_media": {
|
||||||
|
"label": "Rich media",
|
||||||
|
"description": "idk",
|
||||||
|
":max_connections": {
|
||||||
|
"label": "Max connections",
|
||||||
|
"description": "Number workers in the pool."
|
||||||
|
},
|
||||||
|
":timeout": {
|
||||||
|
"label": "Timeout",
|
||||||
|
"description": "Timeout while `hackney` will wait for response."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
":pools": {
|
":pools": {
|
||||||
":rich_media": {
|
":rich_media": {
|
||||||
"label": "Rich media",
|
"label": "Rich media",
|
||||||
"description": "idk"
|
"description": "idk",
|
||||||
|
":size": {
|
||||||
|
"label": "Size",
|
||||||
|
"description": "Maximum number of concurrent requests in the pool."
|
||||||
|
},
|
||||||
|
":max_waiting": {
|
||||||
|
"label": "Max waiting",
|
||||||
|
"description": "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errors when a new request is made"
|
||||||
|
},
|
||||||
|
":recv_timeout": {
|
||||||
|
"label": "Recv timeout",
|
||||||
|
"description": "Timeout for the pool while gun will wait for response"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
":rate_limit": {
|
":rate_limit": {
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ const api = {
|
||||||
// Follow requests
|
// Follow requests
|
||||||
startFetchingFollowRequests(store) {
|
startFetchingFollowRequests(store) {
|
||||||
if (store.state.fetchers.followRequests) return
|
if (store.state.fetchers.followRequests) return
|
||||||
const fetcher = followRequestFetcher.startFetchingFollowRequests({
|
const fetcher = followRequestFetcher.startFetching({
|
||||||
store,
|
store,
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -883,9 +883,13 @@ const statuses = {
|
||||||
'addNewUsers',
|
'addNewUsers',
|
||||||
data.statuses.map((s) => s.user).filter((u) => u),
|
data.statuses.map((s) => s.user).filter((u) => u),
|
||||||
)
|
)
|
||||||
data.statuses = store.commit('addNewStatuses', {
|
store.commit('addNewStatuses', {
|
||||||
statuses: data.statuses,
|
statuses: data.statuses,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
data.statuses = data.statuses.map(
|
||||||
|
(s) => store.state.allStatusesObject[s.id],
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||||
|
|
||||||
|
import { promisedRequest } from 'src/api/helpers.js'
|
||||||
import { ROOT_CONFIG } from 'src/modules/default_config_state.js'
|
import { ROOT_CONFIG } from 'src/modules/default_config_state.js'
|
||||||
|
|
||||||
// On platforms where this is not supported, it will return undefined
|
// On platforms where this is not supported, it will return undefined
|
||||||
|
|
@ -300,7 +301,7 @@ export const applyStyleConfig = (input) => {
|
||||||
adoptStyleSheets()
|
adoptStyleSheets()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
export const getResourcesIndex = async (url, parser = (x) => x) => {
|
||||||
const cache = 'no-store'
|
const cache = 'no-store'
|
||||||
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
|
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
|
||||||
let builtin
|
let builtin
|
||||||
|
|
@ -314,10 +315,11 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
||||||
return [
|
return [
|
||||||
k,
|
k,
|
||||||
() =>
|
() =>
|
||||||
window
|
promisedRequest({
|
||||||
.fetch(v, { cache })
|
url: v,
|
||||||
.then((data) => data.text())
|
cache,
|
||||||
.then((text) => parser(text))
|
})
|
||||||
|
.then(({ data: text }) => parser(text))
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return null
|
return null
|
||||||
|
|
@ -331,18 +333,19 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const builtinData = await window.fetch(url, { cache })
|
const { data: builtinData } = await promisedRequest({ url, cache })
|
||||||
const builtinResources = await builtinData.json()
|
builtin = resourceTransform(builtinData)
|
||||||
builtin = resourceTransform(builtinResources)
|
|
||||||
} catch {
|
} catch {
|
||||||
builtin = []
|
builtin = []
|
||||||
console.warn(`Builtin resources at ${url} unavailable`)
|
console.warn(`Builtin resources at ${url} unavailable`)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const customData = await window.fetch(customUrl, { cache })
|
const { data: customData } = await promisedRequest({
|
||||||
const customResources = await customData.json()
|
url: customUrl,
|
||||||
custom = resourceTransform(customResources)
|
cache,
|
||||||
|
})
|
||||||
|
custom = resourceTransform(customData)
|
||||||
} catch {
|
} catch {
|
||||||
custom = []
|
custom = []
|
||||||
console.warn(`Custom resources at ${customUrl} unavailable`)
|
console.warn(`Custom resources at ${customUrl} unavailable`)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export const useInterfaceStore = defineStore('interface', {
|
||||||
},
|
},
|
||||||
layoutType: 'normal',
|
layoutType: 'normal',
|
||||||
globalNotices: [],
|
globalNotices: [],
|
||||||
|
globalError: null,
|
||||||
layoutHeight: 0,
|
layoutHeight: 0,
|
||||||
lastTimeline: null,
|
lastTimeline: null,
|
||||||
foreignProfileBackground: null,
|
foreignProfileBackground: null,
|
||||||
|
|
@ -176,11 +177,35 @@ export const useInterfaceStore = defineStore('interface', {
|
||||||
removeGlobalNotice(notice) {
|
removeGlobalNotice(notice) {
|
||||||
this.globalNotices = this.globalNotices.filter((n) => n !== notice)
|
this.globalNotices = this.globalNotices.filter((n) => n !== notice)
|
||||||
},
|
},
|
||||||
|
setGlobalError({ error, instance, info }) {
|
||||||
|
console.log(info)
|
||||||
|
switch (info) {
|
||||||
|
case 'https://vuejs.org/error-reference/#runtime-13': {
|
||||||
|
this.globalError = {
|
||||||
|
title: 'general.refresh_required',
|
||||||
|
content: 'general.refresh_required_content',
|
||||||
|
// `true` disables cache on Firefox (non-standard)
|
||||||
|
recover: () => window.location.reload(true),
|
||||||
|
recoverText: 'general.refresh_required_refresh',
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this.globalError = { error }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.globalError)
|
||||||
|
},
|
||||||
|
clearGlobalError() {
|
||||||
|
this.globalError = null
|
||||||
|
},
|
||||||
pushGlobalNotice({
|
pushGlobalNotice({
|
||||||
messageKey,
|
messageKey,
|
||||||
messageArgs = {},
|
messageArgs = {},
|
||||||
level = 'error',
|
level = 'error',
|
||||||
timeout = 0,
|
timeout = 5000,
|
||||||
}) {
|
}) {
|
||||||
const notice = {
|
const notice = {
|
||||||
messageKey,
|
messageKey,
|
||||||
|
|
@ -193,7 +218,7 @@ export const useInterfaceStore = defineStore('interface', {
|
||||||
// Adding a new element to array wraps it in a Proxy, which breaks the comparison
|
// Adding a new element to array wraps it in a Proxy, which breaks the comparison
|
||||||
// TODO: Generate UUID or something instead or relying on !== operator?
|
// TODO: Generate UUID or something instead or relying on !== operator?
|
||||||
const newNotice = this.globalNotices[this.globalNotices.length - 1]
|
const newNotice = this.globalNotices[this.globalNotices.length - 1]
|
||||||
if (timeout) {
|
if (timeout > 0) {
|
||||||
setTimeout(() => this.removeGlobalNotice(newNotice), timeout)
|
setTimeout(() => this.removeGlobalNotice(newNotice), timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-env serviceworker */
|
/* eslint-env serviceworker */
|
||||||
|
|
||||||
|
// biome-ignore: side effect import of assets list
|
||||||
import 'virtual:pleroma-fe/service_worker_env'
|
import 'virtual:pleroma-fe/service_worker_env'
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ describe('Draft saving', () => {
|
||||||
await textarea.setValue('mew mew')
|
await textarea.setValue('mew mew')
|
||||||
wrapper.vm.requestClose()
|
wrapper.vm.requestClose()
|
||||||
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
await waitForEvent(wrapper, 'can-close')
|
await waitForEvent(wrapper, 'close-accepted')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
|
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
|
||||||
|
|
@ -122,7 +122,7 @@ describe('Draft saving', () => {
|
||||||
await textarea.setValue('mew mew')
|
await textarea.setValue('mew mew')
|
||||||
wrapper.vm.requestClose()
|
wrapper.vm.requestClose()
|
||||||
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
await waitForEvent(wrapper, 'can-close')
|
await waitForEvent(wrapper, 'close-accepted')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
|
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
|
||||||
|
|
@ -143,7 +143,7 @@ describe('Draft saving', () => {
|
||||||
const textarea = wrapper.get('textarea')
|
const textarea = wrapper.get('textarea')
|
||||||
await textarea.setValue('mew mew')
|
await textarea.setValue('mew mew')
|
||||||
wrapper.vm.requestClose()
|
wrapper.vm.requestClose()
|
||||||
await waitForEvent(wrapper, 'can-close')
|
await waitForEvent(wrapper, 'close-accepted')
|
||||||
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -180,6 +180,6 @@ describe('Draft saving', () => {
|
||||||
console.info('clicked')
|
console.info('clicked')
|
||||||
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
await waitForEvent(wrapper, 'can-close')
|
await waitForEvent(wrapper, 'close-accepted')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ set -u
|
||||||
COMPOSE_FILE="docker-compose.e2e.yml"
|
COMPOSE_FILE="docker-compose.e2e.yml"
|
||||||
|
|
||||||
: "${COMPOSE_MENU:=false}"
|
: "${COMPOSE_MENU:=false}"
|
||||||
: "${PLEROMA_IMAGE:=git.pleroma.social:5050/pleroma/pleroma:stable}"
|
: "${PLEROMA_IMAGE:=git.pleroma.social/pleroma/pleroma:stable}"
|
||||||
: "${E2E_ADMIN_USERNAME:=admin}"
|
: "${E2E_ADMIN_USERNAME:=admin}"
|
||||||
: "${E2E_ADMIN_PASSWORD:=adminadmin}"
|
: "${E2E_ADMIN_PASSWORD:=adminadmin}"
|
||||||
: "${E2E_ADMIN_EMAIL:=admin@example.com}"
|
: "${E2E_ADMIN_EMAIL:=admin@example.com}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue