refactoring and prettifying

This commit is contained in:
Henry Jameson 2026-07-22 13:50:09 +03:00
commit 62aa0ca365
3 changed files with 190 additions and 150 deletions

View file

@ -1,4 +1,4 @@
import { debounce, reject, uniqBy } from 'lodash'
import { debounce, reject, uniqBy, isEqual, unescape as ldUnescape } from 'lodash'
import { mapActions, mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
@ -68,6 +68,7 @@ const PostStatusForm = {
// Status editing stuff
statusId: String,
statusText: String,
statusSubject: String,
statusIsSensitive: {
type: Boolean,
required: false,
@ -79,30 +80,34 @@ const PostStatusForm = {
statusMediaDescriptions: Object,
statusScope: String,
statusContentType: String,
// Replies/mentions
replyTo: String,
repliedUser: Object,
mentionsLine: Boolean,
mentionsLineReadOnly: Boolean,
attentions: Array,
subject: String,
copyMessageScope: String,
profileMention: String,
replyTo: String, // id of the post replying to
repliedUser: Object, // user object replying to
attentions: Array, // list of users mentioned
repliedScope: String, // scope of the post replying to
repliedSubject: String, // subject of post replying to
profileMention: Boolean, // is mentioning a user (used in profile page -> mention)
// Draft stuff
hideDraft: Boolean,
closeable: Boolean,
draftId: String,
hideDraft: Boolean, // Disable drafts functionality
closeable: Boolean, // Whether form can be closed (i.e. in replies)
draftId: String, // ID of the draft to be used
// Chats stuff
maxHeight: Number,
placeholder: String,
postHandler: Function,
preserveFocus: Boolean,
autoFocus: Boolean,
fileLimit: Number,
postHandler: Function, // Used to override poster to use chats one instead of status one
preserveFocus: Boolean, // Keep focus on form after posting
autoFocus: Boolean, // Steal focus when form is opened
fileLimit: Number, // Chats only support 1 attachment :(
submitOnEnter: Boolean,
emojiPickerPlacement: String,
optimisticPosting: Boolean,
optimisticPosting: Boolean, // Don't wait for confirmation that post is done
// Feature toggles for special cases (mostly chats)
mentionsLine: Boolean, // Use separate field for specifying mentions
mentionsLineReadOnly: Boolean, // Make the said field read-only (for chat conversation view)
disableSubject: Boolean,
disableScopeSelector: Boolean,
disableVisibilitySelector: Boolean,
@ -126,20 +131,19 @@ const PostStatusForm = {
],
data() {
return {
randomSeed: genRandomSeed(),
dropFiles: [],
uploadingFiles: false,
error: null,
posting: false,
highlighted: 0,
initialized: false,
// Data is initialized first, but we have no access to .computed
// so we pre-fill with stuff meant for status editing and later
// back-fill with defaults in .created()
randomSeed: genRandomSeed(),
// Posting stuff
idempotencyKey: '',
/* Data is initialized first, but we have no access to .computed yet
* which we need for some defaults (i.e. user configration)
* so we pre-fill with stuff meant for status editing and later
* back-fill with defaults in .created()
*/
newStatus: {
status: this.statusText ?? null,
mentions: this.statusMentionLine ?? null,
spoilerText: this.subject ?? null,
spoilerText: this.statusSubject ?? null,
quote: this.statusQuote ?? null,
files: this.statusFiles ?? null,
poll: this.statusPoll ?? null,
@ -148,15 +152,25 @@ const PostStatusForm = {
visibility: this.statusVisibility ?? null,
contentType: this.statusContentType ?? null,
},
caret: 0,
// Attachments
dropFiles: [],
uploadingFiles: false,
showDropIcon: 'hide',
dropStopTimeout: null,
// Preview
preview: null,
previewLoading: false,
emojiInputShown: false,
idempotencyKey: '',
// Draft
saveInhibited: true,
saveable: false,
// Misc States
emojiInputShown: false,
error: null,
posting: false,
}
},
components: {
@ -178,6 +192,8 @@ const PostStatusForm = {
Popover,
},
created() {
this.updateIdempotencyKey()
// If we are starting a new post, do not associate it with old drafts
const draft = !this.disableDraft && (this.draftId || this.statusType !== 'new')
? this.getDraft(this.statusType, this.refId)
@ -189,43 +205,14 @@ const PostStatusForm = {
this.newStatus[key] = draft[key] ?? this.newStatus[key]
})
} else {
const defaultNewStatus = {
spoilerText: '',
files: [],
poll: null,
quote: null,
mediaDescriptions: {},
}
const scope =
(this.copyMessageScope && this.userDefaultScopeCopy) ||
this.copyMessageScope === 'direct'
? this.copyMessageScope
: this.userDefaultScope
const preset = this.$route.query.message
let statusText = preset ?? ''
if (this.mentionsLine) {
defaultNewStatus.status = statusText
defaultNewStatus.mentions = this.mentionsString.trim()
} else {
defaultNewStatus.status = this.mentionsString + statusText
defaultNewStatus.mentions = ''
}
defaultNewStatus.nsfw = this.userDefaultSensitive
defaultNewStatus.visibility = scope
defaultNewStatus.contentType = this.userDefaultPostContentType
Object.entries(defaultNewStatus).forEach(([key, value]) => {
Object.entries(this.defaultNewStatus).forEach(([key, value]) => {
this.newStatus[key] = this.newStatus[key] ?? value
})
}
this.initialized = true
},
mounted() {
this.updateIdempotencyKey()
this.resize(this.$refs.textarea)
if (this.replyTo) {
@ -249,7 +236,7 @@ const PostStatusForm = {
return !this.disablePreview && (!!this.preview || this.previewLoading)
},
// Composition stuff
// Composing stuff
statusType() {
if (this.replyTo) {
return 'reply'
@ -290,6 +277,39 @@ const PostStatusForm = {
? this.mentionsString + this.newStatus.status
: this.newStatus.status
},
defaultNewStatus() {
const defaultNewStatus = {
files: [],
poll: null,
quote: null,
mediaDescriptions: {},
}
const scope =
(this.repliedScope && this.userDefaultScopeCopy) ||
this.repliedScope === 'direct'
? this.repliedScope
: this.userDefaultScope
const preset = this.$route.query.message
const statusText = preset ?? ''
if (this.mentionsLine) {
defaultNewStatus.status = statusText
defaultNewStatus.mentions = this.mentionsString.trim()
} else {
defaultNewStatus.status = this.mentionsString + statusText
defaultNewStatus.mentions = ''
}
defaultNewStatus.spoilerText = this.replySubject ?? ''
defaultNewStatus.nsfw = this.userDefaultSensitive
defaultNewStatus.visibility = scope
defaultNewStatus.contentType = this.userDefaultPostContentType
return defaultNewStatus
},
// -Edit
isEdit() {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
},
@ -297,13 +317,26 @@ const PostStatusForm = {
isReply() {
return this.statusType === 'reply'
},
inReplyStatusId() {
inReplyToStatusId() {
return !this.hasQuote ||
!this.newStatus.quote.thread ||
!this.newStatus.quote.id
? this.replyTo
: undefined
},
replySubject() {
if (!this.replySubject) return null
const decodedSummary = ldUnescape(this.replySubject)
const behavior = this.mergedConfig.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary
} else if (behavior === 'email') {
return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
},
// -Poll
hasPoll() {
return this.newStatus.poll != null
@ -406,7 +439,7 @@ const PostStatusForm = {
isOverLengthLimit() {
return this.hasStatusLengthLimit && this.charactersLeft < 0
},
emptyStatus() {
isEmptyStatus() {
return (
this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
)
@ -416,6 +449,13 @@ const PostStatusForm = {
},
// Drafts
isDirty() {
return Object.entries(this.defaultNewStatus).some(([key, defaultValue]) => {
const actualValue = this.newStatus[key]
if (actualValue === null) return false
return !isEqual(actualValue, defaultValue)
})
},
shouldAutoSaveDraft() {
return useMergedConfigStore().mergedConfig.autoSaveDraft
},
@ -525,11 +565,8 @@ const PostStatusForm = {
}),
},
watch: {
newStatus: {
deep: true,
handler() {
if (this.initialized) this.statusChanged()
},
isDirty(newVal, oldVal) {
this.statusChanged()
},
saveable(val) {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes
@ -541,35 +578,16 @@ const PostStatusForm = {
}
},
},
beforeUnmount() {
this.maybeAutoSaveDraft()
this.removeBeforeUnloadListener()
},
methods: {
...mapActions(useMediaViewerStore, ['increment']),
statusChanged() {
this.autoPreview()
this.updateIdempotencyKey()
this.debouncedMaybeAutoSaveDraft()
this.saveable = true
this.saveInhibited = false
},
// Composing
onMentionsLineUpdate(e) {
if (this.mentionsLineReadOnly) return
this.newStatus.mentionsLine = e
},
toggleQuoteForm() {
if (!this.hasQuote) {
this.newStatus.quote = {}
this.newStatus.quote.thread = false
this.newStatus.quote.id = null
this.newStatus.quote.url = ''
} else {
this.newStatus.quote = null
}
changeVis(visibility) {
this.newStatus.visibility = visibility
},
clearStatus() {
const newStatus = this.newStatus
this.saveInhibited = true
this.newStatus.status = ''
this.newStatus.mentionsLine = '',
@ -610,12 +628,12 @@ const PostStatusForm = {
if (
this.optimisticPosting &&
(this.emptyStatus || this.isOverLengthLimit)
(this.isEmptyStatus || this.isOverLengthLimit)
) {
return
}
if (this.emptyStatus) {
if (this.isEmptyStatus) {
this.error = this.$t('post_status.empty_status_error')
return
}
@ -638,12 +656,12 @@ const PostStatusForm = {
const postingOptions = {
status: this.newStatusContent,
spoilerText: newStatus.spoilerText || null,
spoilerText: newStatus.spoilerText ?? null,
visibility: newStatus.visibility,
sensitive: newStatus.nsfw,
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.inReplyStatusId,
inReplyToStatusId: this.inReplyToStatusId,
quoteId: this.quoteId,
contentType: newStatus.contentType,
poll,
@ -667,8 +685,10 @@ const PostStatusForm = {
this.posting = false
})
},
// Preview
previewStatus() {
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
if (this.isEmptyStatus && this.newStatus.spoilerText.trim() === '') {
this.preview = { error: this.$t('post_status.preview_empty') }
this.previewLoading = false
return
@ -684,10 +704,11 @@ const PostStatusForm = {
sensitive: newStatus.nsfw,
media: [],
store: this.$store,
inReplyToStatusId: this.inReplyStatusId,
inReplyToStatusId: this.inReplyToStatusId,
quoteId: this.quoteId,
contentType: newStatus.contentType,
poll: {},
poll: null,
quote: null,
preview: true,
})
.then((data) => {
@ -722,6 +743,21 @@ const PostStatusForm = {
this.previewStatus()
}
},
// Attachments
setMediaDescription(id) {
const description = this.newStatus.mediaDescriptions[id]
if (!description || description.trim() === '') return
return statusPoster.setMediaDescription({
store: this.$store,
id,
description,
})
},
setAllMediaDescriptions() {
const ids = this.newStatus.files.map((file) => file.id)
return Promise.all(ids.map((id) => this.setMediaDescription(id)))
},
addMediaFile(fileInfo) {
this.newStatus.files.push(fileInfo)
this.$emit('resize', { delayed: true })
@ -795,6 +831,9 @@ const PostStatusForm = {
this.showDropIcon = 'show'
}
},
// Auto-sizable input field
// TODO separate into its own component pls
onEmojiInputInput() {
this.$nextTick(() => {
this.resize(this.$refs.textarea)
@ -908,53 +947,39 @@ const PostStatusForm = {
scrollerRef.scrollTop = targetScroll
}
},
clearError() {
this.error = null
},
changeVis(visibility) {
this.newStatus.visibility = visibility
},
// Poll
togglePollForm() {
this.newStatus.poll = this.hasPoll ? null : {}
},
setPoll(poll) {
this.newStatus.poll = poll
},
// Quote
toggleQuoteForm() {
if (!this.hasQuote) {
this.newStatus.quote = {}
this.newStatus.quote.thread = false
this.newStatus.quote.id = null
this.newStatus.quote.url = ''
} else {
this.newStatus.quote = null
}
},
clearQuoteForm() {
if (this.$refs.quoteForm) {
this.$refs.quoteForm.clear()
}
},
dismissScopeNotice() {
useSyncConfigStore().setSimplePrefAndSave({
path: 'hideScopeNotice',
value: true,
})
},
setMediaDescription(id) {
const description = this.newStatus.mediaDescriptions[id]
if (!description || description.trim() === '') return
return statusPoster.setMediaDescription({
store: this.$store,
id,
description,
})
},
setAllMediaDescriptions() {
const ids = this.newStatus.files.map((file) => file.id)
return Promise.all(ids.map((id) => this.setMediaDescription(id)))
},
handleEmojiInputShow(value) {
this.emojiInputShown = value
},
updateIdempotencyKey() {
this.idempotencyKey = Date.now().toString()
},
openProfileTab() {
useInterfaceStore().openSettingsModalTab('profile')
},
propsToNative(props) {
return propsToNative(props)
// Drafts
statusChanged() {
this.autoPreview()
this.updateIdempotencyKey()
this.debouncedMaybeAutoSaveDraft()
this.saveable = true
this.saveInhibited = false
},
saveDraft() {
if (!this.disableDraft && !this.saveInhibited) {
@ -1036,6 +1061,34 @@ const PostStatusForm = {
window.removeEventListener('beforeunload', this._beforeUnloadListener)
}
},
// Misc
propsToNative(props) {
return propsToNative(props)
},
handleEmojiInputShow(value) {
this.emojiInputShown = value
},
updateIdempotencyKey() {
this.idempotencyKey = Date.now().toString()
},
openProfileTab() {
useInterfaceStore().openSettingsModalTab('profile')
},
dismissScopeNotice() {
useSyncConfigStore().setSimplePrefAndSave({
path: 'hideScopeNotice',
value: true,
})
},
clearError() {
this.error = null
},
...mapActions(useMediaViewerStore, ['increment']),
},
beforeUnmount() {
this.maybeAutoSaveDraft()
this.removeBeforeUnloadListener()
},
}

View file

@ -1,4 +1,4 @@
import { unescape as ldUnescape, uniqBy } from 'lodash'
import { uniqBy } from 'lodash'
import { defineAsyncComponent } from 'vue'
import AvatarList from 'src/components/avatar_list/avatar_list.vue'
@ -377,19 +377,6 @@ const Status = {
return user && user.screen_name_ui
}
},
replySubject() {
if (!this.status.summary) return ''
const decodedSummary = ldUnescape(this.status.summary)
const behavior = this.mergedConfig.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary
} else if (behavior === 'email') {
return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
},
combinedFavsAndRepeatsUsers() {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(

View file

@ -549,8 +549,8 @@
:reply-to="status.id"
:attentions="status.attentions"
:replied-user="status.user"
:copy-message-scope="status.visibility"
:subject="replySubject"
:replied-scope="status.visibility"
:replied-subject="status.summary"
@posted="closeReplyForm"
@draft-done="closeReplyForm"
@close-accepted="closeReplyForm"