Merge branch 'feat/rich-text-preview' into 'develop'
Status preview #459 See merge request pleroma/pleroma-fe!1159
This commit is contained in:
commit
9ccc6174a7
9 changed files with 195 additions and 36 deletions
|
@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Autocomplete domains from list of known instances
|
- Autocomplete domains from list of known instances
|
||||||
- 'Bot' settings option and badge
|
- 'Bot' settings option and badge
|
||||||
- Added profile meta data fields that can be set in profile settings
|
- Added profile meta data fields that can be set in profile settings
|
||||||
|
- Added status preview option to preview your statuses before posting
|
||||||
- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style
|
- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
@ -3,9 +3,10 @@ import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||||
import PollForm from '../poll/poll_form.vue'
|
import PollForm from '../poll/poll_form.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
import { reject, map, uniqBy } from 'lodash'
|
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
@ -38,7 +39,8 @@ const PostStatusForm = {
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
PollForm,
|
PollForm,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
StatusContent
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resize(this.$refs.textarea)
|
this.resize(this.$refs.textarea)
|
||||||
|
@ -84,7 +86,9 @@ const PostStatusForm = {
|
||||||
caret: 0,
|
caret: 0,
|
||||||
pollFormVisible: false,
|
pollFormVisible: false,
|
||||||
showDropIcon: 'hide',
|
showDropIcon: 'hide',
|
||||||
dropStopTimeout: null
|
dropStopTimeout: null,
|
||||||
|
preview: null,
|
||||||
|
previewLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -163,19 +167,30 @@ const PostStatusForm = {
|
||||||
this.newStatus.poll &&
|
this.newStatus.poll &&
|
||||||
this.newStatus.poll.error
|
this.newStatus.poll.error
|
||||||
},
|
},
|
||||||
|
showPreview () {
|
||||||
|
return !!this.preview || this.previewLoading
|
||||||
|
},
|
||||||
|
emptyStatus () {
|
||||||
|
return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'newStatus.contentType': function () {
|
||||||
|
this.autoPreview()
|
||||||
|
},
|
||||||
|
'newStatus.spoilerText': function () {
|
||||||
|
this.autoPreview()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postStatus (newStatus) {
|
postStatus (newStatus) {
|
||||||
if (this.posting) { return }
|
if (this.posting) { return }
|
||||||
if (this.submitDisabled) { return }
|
if (this.submitDisabled) { return }
|
||||||
|
if (this.emptyStatus) {
|
||||||
if (this.newStatus.status === '') {
|
this.error = this.$t('post_status.empty_status_error')
|
||||||
if (this.newStatus.files.length === 0) {
|
|
||||||
this.error = 'Cannot post an empty status with no files'
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
||||||
if (this.pollContentError) {
|
if (this.pollContentError) {
|
||||||
|
@ -212,12 +227,64 @@ const PostStatusForm = {
|
||||||
el.style.height = 'auto'
|
el.style.height = 'auto'
|
||||||
el.style.height = undefined
|
el.style.height = undefined
|
||||||
this.error = null
|
this.error = null
|
||||||
|
this.previewStatus()
|
||||||
} else {
|
} else {
|
||||||
this.error = data.error
|
this.error = data.error
|
||||||
}
|
}
|
||||||
this.posting = false
|
this.posting = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
previewStatus () {
|
||||||
|
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
||||||
|
this.preview = { error: this.$t('post_status.preview_empty') }
|
||||||
|
this.previewLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newStatus = this.newStatus
|
||||||
|
this.previewLoading = true
|
||||||
|
statusPoster.postStatus({
|
||||||
|
status: newStatus.status,
|
||||||
|
spoilerText: newStatus.spoilerText || null,
|
||||||
|
visibility: newStatus.visibility,
|
||||||
|
sensitive: newStatus.nsfw,
|
||||||
|
media: [],
|
||||||
|
store: this.$store,
|
||||||
|
inReplyToStatusId: this.replyTo,
|
||||||
|
contentType: newStatus.contentType,
|
||||||
|
poll: {},
|
||||||
|
preview: true
|
||||||
|
}).then((data) => {
|
||||||
|
// Don't apply preview if not loading, because it means
|
||||||
|
// user has closed the preview manually.
|
||||||
|
if (!this.previewLoading) return
|
||||||
|
if (!data.error) {
|
||||||
|
this.preview = data
|
||||||
|
} else {
|
||||||
|
this.preview = { error: data.error }
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
this.preview = { error }
|
||||||
|
}).finally(() => {
|
||||||
|
this.previewLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500),
|
||||||
|
autoPreview () {
|
||||||
|
if (!this.preview) return
|
||||||
|
this.previewLoading = true
|
||||||
|
this.debouncePreviewStatus()
|
||||||
|
},
|
||||||
|
closePreview () {
|
||||||
|
this.preview = null
|
||||||
|
this.previewLoading = false
|
||||||
|
},
|
||||||
|
togglePreview () {
|
||||||
|
if (this.showPreview) {
|
||||||
|
this.closePreview()
|
||||||
|
} else {
|
||||||
|
this.previewStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
addMediaFile (fileInfo) {
|
addMediaFile (fileInfo) {
|
||||||
this.newStatus.files.push(fileInfo)
|
this.newStatus.files.push(fileInfo)
|
||||||
},
|
},
|
||||||
|
@ -239,6 +306,7 @@ const PostStatusForm = {
|
||||||
return fileTypeService.fileType(fileInfo.mimetype)
|
return fileTypeService.fileType(fileInfo.mimetype)
|
||||||
},
|
},
|
||||||
paste (e) {
|
paste (e) {
|
||||||
|
this.autoPreview()
|
||||||
this.resize(e)
|
this.resize(e)
|
||||||
if (e.clipboardData.files.length > 0) {
|
if (e.clipboardData.files.length > 0) {
|
||||||
// prevent pasting of file as text
|
// prevent pasting of file as text
|
||||||
|
@ -273,6 +341,7 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEmojiInputInput (e) {
|
onEmojiInputInput (e) {
|
||||||
|
this.autoPreview()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.resize(this.$refs['textarea'])
|
this.resize(this.$refs['textarea'])
|
||||||
})
|
})
|
||||||
|
|
|
@ -69,6 +69,44 @@
|
||||||
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
||||||
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="preview-heading faint">
|
||||||
|
<a
|
||||||
|
class="preview-toggle faint"
|
||||||
|
@click.stop.prevent="togglePreview"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.preview') }}
|
||||||
|
<i
|
||||||
|
class="icon-down-open"
|
||||||
|
:style="{ transform: showPreview ? 'rotate(0deg)' : 'rotate(-90deg)' }"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<i
|
||||||
|
v-show="previewLoading"
|
||||||
|
class="icon-spin3 animate-spin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showPreview"
|
||||||
|
class="preview-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!preview"
|
||||||
|
class="preview-status"
|
||||||
|
>
|
||||||
|
{{ $t('general.loading') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="preview.error"
|
||||||
|
class="preview-status preview-error"
|
||||||
|
>
|
||||||
|
{{ preview.error }}
|
||||||
|
</div>
|
||||||
|
<StatusContent
|
||||||
|
v-else
|
||||||
|
:status="preview"
|
||||||
|
class="preview-status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
v-if="newStatus.spoilerText || alwaysShowSubject"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
|
@ -77,7 +115,6 @@
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
|
@ -302,14 +339,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-status-form {
|
|
||||||
.visibility-tray {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-status-form {
|
.post-status-form {
|
||||||
.form-bottom {
|
.form-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -336,6 +365,48 @@
|
||||||
max-width: 10em;
|
max-width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-heading {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.icon-spin3 {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-toggle {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-down-open {
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error {
|
||||||
|
font-style: italic;
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
border: 1px solid $fallback--border;
|
||||||
|
border: 1px solid var(--border, $fallback--border);
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
.text-format {
|
.text-format {
|
||||||
.only-format {
|
.only-format {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
|
@ -343,6 +414,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-tray {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.media-upload-icon, .poll-icon, .emoji-icon {
|
.media-upload-icon, .poll-icon, .emoji-icon {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -408,7 +485,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments {
|
.media-upload-wrapper .attachments {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
|
|
||||||
.attachment {
|
.attachment {
|
||||||
|
|
|
@ -377,9 +377,6 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-el {
|
.status-el {
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
border-left-width: 0px;
|
border-left-width: 0px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
|
|
|
@ -217,6 +217,9 @@ $status-margin: 0.75em;
|
||||||
font-family: var(--postFont, sans-serif);
|
font-family: var(--postFont, sans-serif);
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 0.2em 0 0.2em 2em;
|
margin: 0.2em 0 0.2em 2em;
|
||||||
|
|
|
@ -10,10 +10,8 @@
|
||||||
:hide-bio="true"
|
:hide-bio="true"
|
||||||
rounded="top"
|
rounded="top"
|
||||||
/>
|
/>
|
||||||
<div class="panel-footer">
|
|
||||||
<PostStatusForm />
|
<PostStatusForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<auth-form
|
<auth-form
|
||||||
v-else
|
v-else
|
||||||
key="user-panel"
|
key="user-panel"
|
||||||
|
|
|
@ -189,6 +189,9 @@
|
||||||
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
||||||
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
||||||
"posting": "Posting",
|
"posting": "Posting",
|
||||||
|
"preview": "Preview",
|
||||||
|
"preview_empty": "Empty",
|
||||||
|
"empty_status_error": "Can't post an empty status with no files",
|
||||||
"scope_notice": {
|
"scope_notice": {
|
||||||
"public": "This post will be visible to everyone",
|
"public": "This post will be visible to everyone",
|
||||||
"private": "This post will be visible to your followers only",
|
"private": "This post will be visible to your followers only",
|
||||||
|
|
|
@ -645,7 +645,8 @@ const postStatus = ({
|
||||||
poll,
|
poll,
|
||||||
mediaIds = [],
|
mediaIds = [],
|
||||||
inReplyToStatusId,
|
inReplyToStatusId,
|
||||||
contentType
|
contentType,
|
||||||
|
preview
|
||||||
}) => {
|
}) => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
const pollOptions = poll.options || []
|
const pollOptions = poll.options || []
|
||||||
|
@ -675,6 +676,9 @@ const postStatus = ({
|
||||||
if (inReplyToStatusId) {
|
if (inReplyToStatusId) {
|
||||||
form.append('in_reply_to_id', inReplyToStatusId)
|
form.append('in_reply_to_id', inReplyToStatusId)
|
||||||
}
|
}
|
||||||
|
if (preview) {
|
||||||
|
form.append('preview', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(MASTODON_POST_STATUS_URL, {
|
return fetch(MASTODON_POST_STATUS_URL, {
|
||||||
body: form,
|
body: form,
|
||||||
|
@ -682,13 +686,7 @@ const postStatus = ({
|
||||||
headers: authHeaders(credentials)
|
headers: authHeaders(credentials)
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
|
||||||
return response.json()
|
return response.json()
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
error: response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.then((data) => data.error ? data : parseStatus(data))
|
.then((data) => data.error ? data : parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import { map } from 'lodash'
|
import { map } from 'lodash'
|
||||||
import apiService from '../api/api.service.js'
|
import apiService from '../api/api.service.js'
|
||||||
|
|
||||||
const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
|
const postStatus = ({
|
||||||
|
store,
|
||||||
|
status,
|
||||||
|
spoilerText,
|
||||||
|
visibility,
|
||||||
|
sensitive,
|
||||||
|
poll,
|
||||||
|
media = [],
|
||||||
|
inReplyToStatusId = undefined,
|
||||||
|
contentType = 'text/plain',
|
||||||
|
preview = false
|
||||||
|
}) => {
|
||||||
const mediaIds = map(media, 'id')
|
const mediaIds = map(media, 'id')
|
||||||
|
|
||||||
return apiService.postStatus({
|
return apiService.postStatus({
|
||||||
|
@ -13,9 +24,11 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, m
|
||||||
mediaIds,
|
mediaIds,
|
||||||
inReplyToStatusId,
|
inReplyToStatusId,
|
||||||
contentType,
|
contentType,
|
||||||
poll })
|
poll,
|
||||||
|
preview
|
||||||
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error && !preview) {
|
||||||
store.dispatch('addNewStatuses', {
|
store.dispatch('addNewStatuses', {
|
||||||
statuses: [data],
|
statuses: [data],
|
||||||
timeline: 'friends',
|
timeline: 'friends',
|
||||||
|
|
Loading…
Reference in a new issue