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

View file

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