Merge branch 'feat/rich-text-preview' into 'develop'

Status preview #459

See merge request pleroma/pleroma-fe!1159
This commit is contained in:
Shpuld Shpludson 2020-07-06 10:17:26 +00:00
commit 9ccc6174a7
9 changed files with 195 additions and 36 deletions

View file

@ -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
- 'Bot' settings option and badge
- 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
### Changed

View file

@ -3,9 +3,10 @@ import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji_input/emoji_input.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 { 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 { mapGetters } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
@ -38,7 +39,8 @@ const PostStatusForm = {
EmojiInput,
PollForm,
ScopeSelector,
Checkbox
Checkbox,
StatusContent
},
mounted () {
this.resize(this.$refs.textarea)
@ -84,7 +86,9 @@ const PostStatusForm = {
caret: 0,
pollFormVisible: false,
showDropIcon: 'hide',
dropStopTimeout: null
dropStopTimeout: null,
preview: null,
previewLoading: false
}
},
computed: {
@ -163,18 +167,29 @@ const PostStatusForm = {
this.newStatus.poll &&
this.newStatus.poll.error
},
showPreview () {
return !!this.preview || this.previewLoading
},
emptyStatus () {
return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
},
...mapGetters(['mergedConfig'])
},
watch: {
'newStatus.contentType': function () {
this.autoPreview()
},
'newStatus.spoilerText': function () {
this.autoPreview()
}
},
methods: {
postStatus (newStatus) {
if (this.posting) { return }
if (this.submitDisabled) { return }
if (this.newStatus.status === '') {
if (this.newStatus.files.length === 0) {
this.error = 'Cannot post an empty status with no files'
return
}
if (this.emptyStatus) {
this.error = this.$t('post_status.empty_status_error')
return
}
const poll = this.pollFormVisible ? this.newStatus.poll : {}
@ -212,12 +227,64 @@ const PostStatusForm = {
el.style.height = 'auto'
el.style.height = undefined
this.error = null
this.previewStatus()
} else {
this.error = data.error
}
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) {
this.newStatus.files.push(fileInfo)
},
@ -239,6 +306,7 @@ const PostStatusForm = {
return fileTypeService.fileType(fileInfo.mimetype)
},
paste (e) {
this.autoPreview()
this.resize(e)
if (e.clipboardData.files.length > 0) {
// prevent pasting of file as text
@ -273,6 +341,7 @@ const PostStatusForm = {
}
},
onEmojiInputInput (e) {
this.autoPreview()
this.$nextTick(() => {
this.resize(this.$refs['textarea'])
})

View file

@ -69,6 +69,44 @@
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</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
v-if="newStatus.spoilerText || alwaysShowSubject"
v-model="newStatus.spoilerText"
@ -77,7 +115,6 @@
class="form-control"
>
<input
v-model="newStatus.spoilerText"
type="text"
: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 {
.form-bottom {
display: flex;
@ -336,6 +365,48 @@
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 {
.only-format {
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 {
font-size: 26px;
flex: 1;
@ -408,7 +485,7 @@
flex-direction: column;
}
.attachments {
.media-upload-wrapper .attachments {
padding: 0 0.5em;
.attachment {

View file

@ -377,9 +377,6 @@ $status-margin: 0.75em;
}
.status-el {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
min-width: 0;
border-color: $fallback--border;

View file

@ -217,6 +217,9 @@ $status-margin: 0.75em;
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
blockquote {
margin: 0.2em 0 0.2em 2em;

View file

@ -10,9 +10,7 @@
:hide-bio="true"
rounded="top"
/>
<div class="panel-footer">
<PostStatusForm />
</div>
<PostStatusForm />
</div>
<auth-form
v-else

View file

@ -189,6 +189,9 @@
"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.",
"posting": "Posting",
"preview": "Preview",
"preview_empty": "Empty",
"empty_status_error": "Can't post an empty status with no files",
"scope_notice": {
"public": "This post will be visible to everyone",
"private": "This post will be visible to your followers only",

View file

@ -645,7 +645,8 @@ const postStatus = ({
poll,
mediaIds = [],
inReplyToStatusId,
contentType
contentType,
preview
}) => {
const form = new FormData()
const pollOptions = poll.options || []
@ -675,6 +676,9 @@ const postStatus = ({
if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId)
}
if (preview) {
form.append('preview', 'true')
}
return fetch(MASTODON_POST_STATUS_URL, {
body: form,
@ -682,13 +686,7 @@ const postStatus = ({
headers: authHeaders(credentials)
})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {
error: response
}
}
return response.json()
})
.then((data) => data.error ? data : parseStatus(data))
}

View file

@ -1,7 +1,18 @@
import { map } from 'lodash'
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')
return apiService.postStatus({
@ -13,9 +24,11 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, m
mediaIds,
inReplyToStatusId,
contentType,
poll })
poll,
preview
})
.then((data) => {
if (!data.error) {
if (!data.error && !preview) {
store.dispatch('addNewStatuses', {
statuses: [data],
timeline: 'friends',