diff --git a/src/api/user.js b/src/api/user.js index 17e13195d..70929b162 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -207,7 +207,7 @@ export const postStatus = ({ idempotencyKey, }) => { const form = new FormData() - const pollOptions = poll.options || [] + const pollOptions = poll?.options || [] form.append('status', status) form.append('source', 'Pleroma FE') diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index 580cb183f..a71014078 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -1,8 +1,8 @@ -import { find } from 'lodash' import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'vuex' import Attachment from 'src/components/attachment/attachment.vue' +import MentionLink from 'src/components/mention_link/mention_link.vue' import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue' import Gallery from 'src/components/gallery/gallery.vue' import LinkPreview from 'src/components/link-preview/link-preview.vue' @@ -10,6 +10,7 @@ import Popover from 'src/components/popover/popover.vue' import StatusActionButtons from 'src/components/status_action_buttons/status_action_buttons.vue' import StatusBody from 'src/components/status_body/status_body.vue' import StatusContent from 'src/components/status_content/status_content.vue' +import StatusPopover from 'src/components/status_popover/status_popover.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserPopover from 'src/components/user_popover/user_popover.vue' @@ -22,9 +23,10 @@ import { faCircleNotch, faEllipsisH, faTimes, + faReply, } from '@fortawesome/free-solid-svg-icons' -library.add(faTimes, faEllipsisH, faCircleNotch) +library.add(faTimes, faEllipsisH, faCircleNotch, faReply) const ChatMessage = { name: 'ChatMessage', @@ -35,6 +37,8 @@ const ChatMessage = { 'chatItem', 'previousItem', 'hoveredMessageChain', + 'focused', + 'repliedTo', ], emits: ['hover', 'replyRequested'], components: { @@ -48,6 +52,8 @@ const ChatMessage = { LinkPreview, ChatMessageDate, UserPopover, + StatusPopover, + MentionLink, }, computed: { // Returns HH:MM (hours and minutes) in local time. @@ -89,6 +95,25 @@ const ChatMessage = { this.chatItem.data.in_reply_to_status_id ] }, + replyToName() { + if (this.message.in_reply_to_screen_name) { + return this.message.in_reply_to_screen_name + } else { + const user = this.$store.getters.findUser( + this.status.in_reply_to_user_id, + ) + return user && user.screen_name_ui + } + }, + replyProfileLink() { + if (this.isCustomReply) { + const user = this.$store.getters.findUser( + this.message.in_reply_to_user_id, + ) + // FIXME Why user not found sometimes??? + return user ? user.statusnet_profile_url : 'NOT_FOUND' + } + }, messageForStatusContent() { return { summary: '', @@ -101,6 +126,14 @@ const ChatMessage = { hasAttachment() { return this.message.attachments.length > 0 }, + classnames() { + return { + '-outgoing': this.isCurrentUser, + '-incoming': !this.isCurrentUser, + '-pending': this.message.pending, + '-focused': this.focused, + } + }, ...mapPiniaState(useInterfaceStore, { betterShadow: (store) => store.browserSupport.cssFilter, }), diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 34382c42d..0d5db41b2 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -17,7 +17,7 @@ position: absolute; top: -0.8em; right: 0.4rem; - z-index: 10; + z-index: 1; .quick-action-buttons { justify-items: end; @@ -39,23 +39,16 @@ } .reply-to-header { - white-space: nowrap; - width: 100%; display: flex; - align-items: baseline; - gap: 0.5em; + gap: 0.5rem; + align-items: center; + padding: 0.125em 0; + width: 100%; + } - .reply-label { - display: inline-block; - line-height: 1; - } - - .reply-body { - line-height: 1; - display: inline-block; - overflow-x: hidden; - text-overflow: ellipsis; - } + .avatar-spacer { + flex: 0 0 2.2rem; + width: 2.2rem; } .popover { @@ -73,8 +66,7 @@ } .avatar-wrapper { - margin-right: 0.72em; - width: 32px; + margin-right: 0.5em; } .link-preview, @@ -124,27 +116,70 @@ } } + .message-bubble-wrapper { + display: flex; + } + .chat-message-inner { display: flex; flex-direction: column; align-items: flex-start; - max-width: 80%; - min-width: 10em; - width: 100%; } - .-outgoing { + .reply-indicator { display: flex; - flex-flow: row wrap; - place-content: end flex-end; + place-items: center; + place-content: center; + padding: 0.25em; + margin: var(--roundness) 0; + background: var(--border); + border-radius: var(--roundness); + border: 1px solid var(--border); + } - .chat-message-inner { - align-items: flex-end; + .end-spacer { + flex: 1 1 0; + min-width: calc(2.2em + 0.5em + 2em); + } + + &.-incoming { + .reply-indicator { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + } + + .reply-to-popover { + white-space: nowrap; + } + + .reply-label { + white-space: nowrap; + } + + &.-outgoing { + &, + .message-bubble-wrapper, + .chat-message{ + flex-direction: row-reverse; } .reply-to-header { justify-content: end; } + + .reply-indicator { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + + .icon { + transform: scaleX(-1); + } + } + + .chat-message-inner { + align-items: flex-end; + } } .chat-message-inner.with-media { diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js index f7632bc6f..67f0ae6c1 100644 --- a/src/components/chat_message/chat_message.style.js +++ b/src/components/chat_message/chat_message.style.js @@ -4,6 +4,9 @@ export default { variants: { outgoing: '.outgoing', }, + states: { + focused: '.-focused', + }, validInnerComponents: ['Text', 'Icon', 'Border', 'PollGraph'], defaultRules: [ { @@ -18,5 +21,11 @@ export default { background: '--bg, 5', }, }, + { + state: ['focused'], + directives: { + background: '--inheritedBackground, 10', + }, + }, ], } diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 91ad9a4eb..234d20cb2 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -2,13 +2,64 @@
+ + + +
+
- - - - {{ $t('status.reply_to') }} - - - - -
+
- -
- +
- - - -
- - + +
+
+ +
+
diff --git a/src/components/chat_message_list/chat_message_list.js b/src/components/chat_message_list/chat_message_list.js index 4babea5a7..a51e6a0e4 100644 --- a/src/components/chat_message_list/chat_message_list.js +++ b/src/components/chat_message_list/chat_message_list.js @@ -14,6 +14,8 @@ const ChatMessageList = { default: [], }, headerDate: Boolean, + focusedId: String, + repliedId: String, }, data() { return { diff --git a/src/components/chat_message_list/chat_message_list.vue b/src/components/chat_message_list/chat_message_list.vue index cd1688a8c..5cdbf6871 100644 --- a/src/components/chat_message_list/chat_message_list.vue +++ b/src/components/chat_message_list/chat_message_list.vue @@ -6,6 +6,8 @@ :chat-item="chatItem" :previous-item="getPreviousItem(index)" :hovered-message-chain="chatItem.messageChainId === hoveredMessageChainId" + :focused="chatItem.id === focusedId" + :repliedTo="chatItem.id === repliedId" @hover="onMessageHover" @delete="onMessageDelete" @reply-requested="onReplyRequested" diff --git a/src/components/conversation/conversation.scss b/src/components/conversation/conversation.scss index 8bd391703..9f9c30747 100644 --- a/src/components/conversation/conversation.scss +++ b/src/components/conversation/conversation.scss @@ -99,10 +99,10 @@ h4 { margin: 0.5em 0; - line-height: 1.5; + line-height: 1.1; button { - line-height: 1.5; + line-height: 1.1; } } @@ -110,6 +110,9 @@ display: flex; align-items: center; gap: 0.5em; + border: 1px solid var(--border); + border-radius: var(--roundness); + padding: 0.5em; } } @@ -119,5 +122,9 @@ flex-direction: column; align-items: stretch; bottom: 0; + + .reply-form form { + margin-top: 0; + } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index bad0277fd..19d63fbbe 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -202,6 +202,8 @@ >
@@ -211,9 +213,7 @@ class="chat-view-reply-form panel-footer -flexible-height" >
-

+

{{ explicitReplyStatus ? $t('status.reply_to_selected') : $t('status.reply_to_last') }}

-
- - -
+
diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index 22311d348..5d0334a63 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -30,7 +30,10 @@ export default { return pollFallback(this.modelValue, 'pollType') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, pollType: newVal }) + this.$emit('update:modelValue', { + ...this.modelValue, + pollType: newVal, + }) }, }, options: { @@ -39,14 +42,17 @@ export default { }, set(newVal) { this.$emit('update:modelValue', { ...this.modelValue, options: newVal }) - } + }, }, expiryAmount: { get() { return pollFallback(this.modelValue, 'expiryAmount') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, expiryAmount: newVal }) + this.$emit('update:modelValue', { + ...this.modelValue, + expiryAmount: newVal, + }) }, }, expiryUnit: { @@ -54,7 +60,10 @@ export default { return pollFallback(this.modelValue, 'expiryUnit') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, expiryUnit: newVal }) + this.$emit('update:modelValue', { + ...this.modelValue, + expiryUnit: newVal, + }) }, }, pollLimits() { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index c98454342..a34e9a3ec 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -1,4 +1,10 @@ -import { debounce, reject, uniqBy } from 'lodash' +import { + debounce, + isEqual, + unescape as ldUnescape, + reject, + uniqBy, +} from 'lodash' import { mapActions, mapState } from 'pinia' import { defineAsyncComponent } from 'vue' @@ -60,14 +66,12 @@ const pxStringToNumber = (str) => { return Number(str.substring(0, str.length - 2)) } -const DEFAULT_NEWSTATUS = { -} - const PostStatusForm = { props: { // Status editing stuff statusId: String, statusText: String, + statusSubject: String, statusIsSensitive: { type: Boolean, required: false, @@ -79,30 +83,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 +134,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 +155,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,8 +195,11 @@ 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') + const draft = + !this.disableDraft && (this.draftId || this.statusType !== 'new') ? this.getDraft(this.statusType, this.refId) : null @@ -189,43 +209,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 +240,7 @@ const PostStatusForm = { return !this.disablePreview && (!!this.preview || this.previewLoading) }, - // Composition stuff + // Composing stuff statusType() { if (this.replyTo) { return 'reply' @@ -264,16 +255,17 @@ const PostStatusForm = { refId() { if (this.replyTo) { return this.replyTo - } else if (profileMention) { + } else if (this.profileMention && this.repliedUser?.id) { return this.profileMention && this.repliedUser?.id - } else if (statusId) { + } else if (this.statusId) { return this.statusId } else { return null } }, mentionsString() { - if (this.statusType !== 'reply' && this.statusType !== 'mention') return '' + if (this.statusType !== 'reply' && this.statusType !== 'mention') + return '' let allAttentions = [...(this.attentions || [])] allAttentions.unshift(this.repliedUser) @@ -281,7 +273,9 @@ const PostStatusForm = { allAttentions = uniqBy(allAttentions, 'id') allAttentions = reject(allAttentions, { id: this.currentUser.id }) - const mentions = allAttentions.map((attention) => `@${attention.screen_name}`) + const mentions = allAttentions.map( + (attention) => `@${attention.screen_name}`, + ) return mentions.length > 0 ? mentions.join(' ') + ' ' : '' }, @@ -290,6 +284,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.repliedSubjectString ?? '' + 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 +324,26 @@ const PostStatusForm = { isReply() { return this.statusType === 'reply' }, - inReplyStatusId() { + inReplyToStatusId() { return !this.hasQuote || !this.newStatus.quote.thread || !this.newStatus.quote.id ? this.replyTo : undefined }, + repliedSubjectString() { + if (!this.repliedSubject) return null + const decodedSummary = ldUnescape(this.repliedSubject) + 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 +446,7 @@ const PostStatusForm = { isOverLengthLimit() { return this.hasStatusLengthLimit && this.charactersLeft < 0 }, - emptyStatus() { + isEmptyStatus() { return ( this.newStatus.status.trim() === '' && this.newStatus.files.length === 0 ) @@ -416,6 +456,15 @@ 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 +574,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,45 +587,25 @@ 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 = '', - this.newStatus.spoilerText = '', - this.newStatus.files = [], - this.newStatus.poll = null, - this.newStatus.quote = null, - this.newStatus.mediaDescriptions = {}, - - this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() + ;(this.newStatus.mentionsLine = ''), + (this.newStatus.spoilerText = ''), + (this.newStatus.files = []), + (this.newStatus.poll = null), + (this.newStatus.quote = null), + (this.newStatus.mediaDescriptions = {}), + this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() this.clearQuoteForm() if (this.preserveFocus) { this.$nextTick(() => { @@ -610,12 +636,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 +664,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 +693,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 +712,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 +751,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 +839,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 +955,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 +1069,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() }, } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index f8d34f73c..aef20e0cc 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -233,7 +233,7 @@ v-if="!disableVisibilitySelector" :show-all="showAllScopes" :user-default="userDefaultScope" - :original-scope="copyMessageScope" + :original-scope="repliedScope" :initial-scope="newStatus.visibility" :on-scope-change="changeVis" /> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 432250548..eb73e263c 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -479,7 +479,8 @@ export default { > {this.collapse ? pass2.map((x) => { - if (!Array.isArray(x)) return x.replace(/\n/g, ' ') + if (typeof x === 'string') return x.replace(/\n/g, ' ') + if (!Array.isArray(x)) return x return x.map((y) => (y.type === 'br' ? ' ' : y)) }) : pass2} diff --git a/src/components/status/status.js b/src/components/status/status.js index 29493fb09..b9359b399 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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( diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 59cd6aaca..3ddcf4e96 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -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" diff --git a/src/modules/users.js b/src/modules/users.js index 22d5be30d..8777f18da 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -45,10 +45,10 @@ import { } from 'src/api/public.js' import { blockUser as apiBlockUser, + editUserNote as apiEditUserNote, muteUser as apiMuteUser, unblockUser as apiUnblockUser, unmuteUser as apiUnmuteUser, - editUserNote as apiEditUserNote, fetchBlocks, fetchDomainMutes, fetchMutes,