add rich text preview

This commit is contained in:
Shpuld Shpuldson 2020-06-28 12:16:41 +03:00
parent 391f796cb4
commit 223fabfe90
5 changed files with 180 additions and 23 deletions

View file

@ -3,6 +3,7 @@ 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 } from 'lodash'
@ -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,8 +167,20 @@ const PostStatusForm = {
this.newStatus.poll && this.newStatus.poll &&
this.newStatus.poll.error this.newStatus.poll.error
}, },
showPreview () {
return !!this.preview || this.previewLoading
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: {
'newStatus.contentType': function (newType) {
if (newType === 'text/plain') {
this.closePreview()
} else if (this.preview) {
this.previewStatus(this.newStatus)
}
}
},
methods: { methods: {
postStatus (newStatus) { postStatus (newStatus) {
if (this.posting) { return } if (this.posting) { return }
@ -218,6 +234,38 @@ const PostStatusForm = {
this.posting = false this.posting = false
}) })
}, },
previewStatus (newStatus) {
this.previewLoading = true
statusPoster.postStatus({
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
visibility: newStatus.visibility,
sensitive: newStatus.nsfw,
media: newStatus.files,
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
})
},
closePreview () {
this.preview = null
this.previewLoading = false
},
addMediaFile (fileInfo) { addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo) this.newStatus.files.push(fileInfo)
}, },

View file

@ -16,6 +16,58 @@
@drop.stop="fileDrop" @drop.stop="fileDrop"
/> />
<div class="form-group"> <div class="form-group">
<a
v-if="newStatus.contentType !== 'text/plain' && !showPreview"
class="preview-start faint"
type="button"
@click.stop.prevent="previewStatus(newStatus)"
>
{{ $t('status.preview') }}
</a>
<div
v-if="showPreview && newStatus.contentType !== 'text/plain'"
class="preview-container"
>
<span class="preview-heading">
<span class="faint preview-title">
{{ $t('status.status_preview') }}
</span>
<i
v-if="previewLoading"
class="icon-spin3 animate-spin"
/>
<a
v-else
class="faint preview-update"
@click.stop.prevent="previewStatus(newStatus)"
>
{{ $t('status.preview_update') }}
</a>
<a
class="preview-close"
@click.stop.prevent="closePreview"
>
<i class="icon-cancel" />
</a>
</span>
<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>
<i18n <i18n
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
path="post_status.account_not_locked_warning" path="post_status.account_not_locked_warning"
@ -77,7 +129,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 +353,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 +379,52 @@
max-width: 10em; max-width: 10em;
} }
.preview-start {
margin-left: auto;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.preview-container {
margin-bottom: 1em;
}
.preview-heading {
display: flex;
width: 100%;
}
.preview-title {
flex-grow: 1;
}
.preview-close {
margin-left: 0.5em;
}
.preview-update {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.preview-error {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
font-style: italic;
}
.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;
}
.text-format { .text-format {
.only-format { .only-format {
color: $fallback--faint; color: $fallback--faint;
@ -343,6 +432,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;

View file

@ -636,7 +636,10 @@
"status_unavailable": "Status unavailable", "status_unavailable": "Status unavailable",
"copy_link": "Copy link to status", "copy_link": "Copy link to status",
"thread_muted": "Thread muted", "thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:" "thread_muted_and_words": ", has words:",
"preview": "Preview",
"status_preview": "Status preview",
"preview_update": "Update"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View file

@ -617,7 +617,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 || []
@ -647,6 +648,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,
@ -654,13 +658,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))
} }

View file

@ -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',