diff options
Diffstat (limited to 'src/components/post_status_form')
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 210 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 413 |
2 files changed, 394 insertions, 229 deletions
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index cbd2024a..40bbf6d4 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -2,17 +2,19 @@ import statusPoster from '../../services/status_poster/status_poster.service.js' 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 StickerPicker from '../sticker_picker/sticker_picker.vue' import fileTypeService from '../../services/file_type/file_type.service.js' -import Completion from '../../services/completion/completion.js' -import { take, filter, reject, map, uniqBy } from 'lodash' +import { reject, map, uniqBy } from 'lodash' +import suggestor from '../emoji-input/suggestor.js' -const buildMentionsString = ({user, attentions}, currentUser) => { +const buildMentionsString = ({ user, attentions }, currentUser) => { let allAttentions = [...attentions] allAttentions.unshift(user) allAttentions = uniqBy(allAttentions, 'id') - allAttentions = reject(allAttentions, {id: currentUser.id}) + allAttentions = reject(allAttentions, { id: currentUser.id }) let mentions = map(allAttentions, (attention) => { return `@${attention.screen_name}` @@ -31,8 +33,10 @@ const PostStatusForm = { ], components: { MediaUpload, - ScopeSelector, - EmojiInput + EmojiInput, + PollForm, + StickerPicker, + ScopeSelector }, mounted () { this.resize(this.$refs.textarea) @@ -48,17 +52,17 @@ const PostStatusForm = { let statusText = preset || '' const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined' - ? this.$store.state.instance.scopeCopy - : this.$store.state.config.scopeCopy + ? this.$store.state.instance.scopeCopy + : this.$store.state.config.scopeCopy if (this.replyTo) { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } - const scope = (this.copyMessageScope && scopeCopy || this.copyMessageScope === 'direct') - ? this.copyMessageScope - : this.$store.state.users.currentUser.default_scope + const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') + ? this.copyMessageScope + : this.$store.state.users.currentUser.default_scope const contentType = typeof this.$store.state.config.postContentType === 'undefined' ? this.$store.state.instance.postContentType @@ -75,57 +79,16 @@ const PostStatusForm = { status: statusText, nsfw: false, files: [], + poll: {}, visibility: scope, contentType }, - caret: 0 + caret: 0, + pollFormVisible: false, + stickerPickerVisible: false } }, computed: { - candidates () { - const firstchar = this.textAtCaret.charAt(0) - if (firstchar === '@') { - const query = this.textAtCaret.slice(1).toUpperCase() - const matchedUsers = filter(this.users, (user) => { - return user.screen_name.toUpperCase().startsWith(query) || - user.name && user.name.toUpperCase().startsWith(query) - }) - if (matchedUsers.length <= 0) { - return false - } - // eslint-disable-next-line camelcase - return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({ - // eslint-disable-next-line camelcase - screen_name: `@${screen_name}`, - name: name, - img: profile_image_url_original, - highlighted: index === this.highlighted - })) - } else if (firstchar === ':') { - if (this.textAtCaret === ':') { return } - const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1))) - if (matchedEmoji.length <= 0) { - return false - } - return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({ - screen_name: `:${shortcode}:`, - name: '', - utf: utf || '', - // eslint-disable-next-line camelcase - img: utf ? '' : this.$store.state.instance.server + image_url, - highlighted: index === this.highlighted - })) - } else { - return false - } - }, - textAtCaret () { - return (this.wordAtCaret || {}).word || '' - }, - wordAtCaret () { - const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {} - return word - }, users () { return this.$store.state.users.users }, @@ -134,10 +97,28 @@ const PostStatusForm = { }, showAllScopes () { const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined' - ? this.$store.state.instance.minimalScopesMode - : this.$store.state.config.minimalScopesMode + ? this.$store.state.instance.minimalScopesMode + : this.$store.state.config.minimalScopesMode return !minimalScopesMode }, + emojiUserSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ], + users: this.$store.state.users.users, + updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + }) + }, + emojiSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ] + }) + }, emoji () { return this.$store.state.instance.emoji || [] }, @@ -174,71 +155,32 @@ const PostStatusForm = { return true } }, - formattingOptionsEnabled () { - return this.$store.state.instance.formattingOptionsEnabled - }, postFormats () { return this.$store.state.instance.postFormats || [] }, safeDMEnabled () { return this.$store.state.instance.safeDM }, + stickersAvailable () { + if (this.$store.state.instance.stickers) { + return this.$store.state.instance.stickers.length > 0 + } + return 0 + }, + pollsAvailable () { + return this.$store.state.instance.pollsAvailable && + this.$store.state.instance.pollLimits.max_options >= 2 + }, hideScopeNotice () { return this.$store.state.config.hideScopeNotice + }, + pollContentError () { + return this.pollFormVisible && + this.newStatus.poll && + this.newStatus.poll.error } }, methods: { - replace (replacement) { - this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) - const el = this.$el.querySelector('textarea') - el.focus() - this.caret = 0 - }, - replaceCandidate (e) { - const len = this.candidates.length || 0 - if (this.textAtCaret === ':' || e.ctrlKey) { return } - if (len > 0) { - e.preventDefault() - const candidate = this.candidates[this.highlighted] - const replacement = candidate.utf || (candidate.screen_name + ' ') - this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) - const el = this.$el.querySelector('textarea') - el.focus() - this.caret = 0 - this.highlighted = 0 - } - }, - cycleBackward (e) { - const len = this.candidates.length || 0 - if (len > 0) { - e.preventDefault() - this.highlighted -= 1 - if (this.highlighted < 0) { - this.highlighted = this.candidates.length - 1 - } - } else { - this.highlighted = 0 - } - }, - cycleForward (e) { - const len = this.candidates.length || 0 - if (len > 0) { - if (e.shiftKey) { return } - e.preventDefault() - this.highlighted += 1 - if (this.highlighted >= len) { - this.highlighted = 0 - } - } else { - this.highlighted = 0 - } - }, - onKeydown (e) { - e.stopPropagation() - }, - setCaret ({target: {selectionStart}}) { - this.caret = selectionStart - }, postStatus (newStatus) { if (this.posting) { return } if (this.submitDisabled) { return } @@ -252,6 +194,12 @@ const PostStatusForm = { } } + const poll = this.pollFormVisible ? this.newStatus.poll : {} + if (this.pollContentError) { + this.error = this.pollContentError + return + } + this.posting = true statusPoster.postStatus({ status: newStatus.status, @@ -261,7 +209,8 @@ const PostStatusForm = { media: newStatus.files, store: this.$store, inReplyToStatusId: this.replyTo, - contentType: newStatus.contentType + contentType: newStatus.contentType, + poll }).then((data) => { if (!data.error) { this.newStatus = { @@ -269,9 +218,13 @@ const PostStatusForm = { spoilerText: '', files: [], visibility: newStatus.visibility, - contentType: newStatus.contentType + contentType: newStatus.contentType, + poll: {} } + this.pollFormVisible = false + this.stickerPickerVisible = false this.$refs.mediaUpload.clearFile() + this.clearPollForm() this.$emit('posted') let el = this.$el.querySelector('textarea') el.style.height = 'auto' @@ -286,6 +239,7 @@ const PostStatusForm = { addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) this.enableSubmit() + this.stickerPickerVisible = false }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) @@ -317,7 +271,7 @@ const PostStatusForm = { }, fileDrop (e) { if (e.dataTransfer.files.length > 0) { - e.preventDefault() // allow dropping text like before + e.preventDefault() // allow dropping text like before this.dropFiles = e.dataTransfer.files } }, @@ -327,8 +281,11 @@ const PostStatusForm = { resize (e) { const target = e.target || e if (!(target instanceof window.Element)) { return } - const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + - Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) + const topPaddingStr = window.getComputedStyle(target)['padding-top'] + const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom'] + // Remove "px" at the end of the values + const vertPadding = Number(topPaddingStr.substr(0, topPaddingStr.length - 2)) + + Number(bottomPaddingStr.substr(0, bottomPaddingStr.length - 2)) // Auto is needed to make textbox shrink when removing lines target.style.height = 'auto' target.style.height = `${target.scrollHeight - vertPadding}px` @@ -342,6 +299,25 @@ const PostStatusForm = { changeVis (visibility) { this.newStatus.visibility = visibility }, + toggleStickerPicker () { + this.stickerPickerVisible = !this.stickerPickerVisible + }, + clearStickerPicker () { + if (this.$refs.stickerPicker) { + this.$refs.stickerPicker.clear() + } + }, + togglePollForm () { + this.pollFormVisible = !this.pollFormVisible + }, + setPoll (poll) { + this.newStatus.poll = poll + }, + clearPollForm () { + if (this.$refs.pollForm) { + this.$refs.pollForm.clear() + } + }, dismissScopeNotice () { this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true }) } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 25c5284f..d29d47e4 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -1,127 +1,268 @@ <template> -<div class="post-status-form"> - <form @submit.prevent="postStatus(newStatus)"> - <div class="form-group" > - <i18n - v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" - path="post_status.account_not_locked_warning" - tag="p" - class="visibility-notice"> - <router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link> - </i18n> - <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" class="visibility-notice notice-dismissible"> - <span>{{ $t('post_status.scope_notice.public') }}</span> - <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss"> - <i class='icon-cancel'></i> - </a> - </p> - <p v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" class="visibility-notice notice-dismissible"> - <span>{{ $t('post_status.scope_notice.unlisted') }}</span> - <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss"> - <i class='icon-cancel'></i> - </a> - </p> - <p v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" class="visibility-notice notice-dismissible"> - <span>{{ $t('post_status.scope_notice.private') }}</span> - <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss"> - <i class='icon-cancel'></i> - </a> - </p> - <p v-else-if="newStatus.visibility === 'direct'" class="visibility-notice"> - <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> - <EmojiInput - v-if="newStatus.spoilerText || alwaysShowSubject" - type="text" - :placeholder="$t('post_status.content_warning')" - v-model="newStatus.spoilerText" - classname="form-control" - /> - <textarea - ref="textarea" - @click="setCaret" - @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" - @keydown="onKeydown" - @keydown.down="cycleForward" - @keydown.up="cycleBackward" - @keydown.shift.tab="cycleBackward" - @keydown.tab="cycleForward" - @keydown.enter="replaceCandidate" - @keydown.meta.enter="postStatus(newStatus)" - @keyup.ctrl.enter="postStatus(newStatus)" - @drop="fileDrop" - @dragover.prevent="fileDrag" - @input="resize" - @paste="paste" - :disabled="posting" - > - </textarea> - <div class="visibility-tray"> - <div class="text-format" v-if="formattingOptionsEnabled"> - <label for="post-content-type" class="select"> - <select id="post-content-type" v-model="newStatus.contentType" class="form-control"> - <option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat"> - {{$t(`post_status.content_type["${postFormat}"]`)}} - </option> - </select> - <i class="icon-down-open"></i> - </label> - </div> + <div class="post-status-form"> + <form + autocomplete="off" + @submit.prevent="postStatus(newStatus)" + > + <div class="form-group"> + <i18n + v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" + path="post_status.account_not_locked_warning" + tag="p" + class="visibility-notice" + > + <router-link :to="{ name: 'user-settings' }"> + {{ $t('post_status.account_not_locked_warning_link') }} + </router-link> + </i18n> + <p + v-if="!hideScopeNotice && newStatus.visibility === 'public'" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.public') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.unlisted') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.private') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="newStatus.visibility === 'direct'" + class="visibility-notice" + > + <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> + <EmojiInput + v-if="newStatus.spoilerText || alwaysShowSubject" + v-model="newStatus.spoilerText" + :suggest="emojiSuggestor" + class="form-control" + > + <input - <scope-selector - :showAll="showAllScopes" - :userDefault="userDefaultScope" - :originalScope="copyMessageScope" - :initialScope="newStatus.visibility" - :onScopeChange="changeVis"/> - </div> - </div> - <div class="autocomplete-panel" v-if="candidates"> - <div class="autocomplete-panel-body"> + v-model="newStatus.spoilerText" + type="text" + :placeholder="$t('post_status.content_warning')" + class="form-post-subject" + > + </EmojiInput> + <EmojiInput + v-model="newStatus.status" + :suggest="emojiUserSuggestor" + class="form-control main-input" + > + <textarea + ref="textarea" + v-model="newStatus.status" + :placeholder="$t('post_status.default')" + rows="1" + :disabled="posting" + class="form-post-body" + @keydown.meta.enter="postStatus(newStatus)" + @keyup.ctrl.enter="postStatus(newStatus)" + @drop="fileDrop" + @dragover.prevent="fileDrag" + @input="resize" + @paste="paste" + /> + <p + v-if="hasStatusLengthLimit" + class="character-counter faint" + :class="{ error: isOverLengthLimit }" + > + {{ charactersLeft }} + </p> + </EmojiInput> + <div class="visibility-tray"> + <scope-selector + :show-all="showAllScopes" + :user-default="userDefaultScope" + :original-scope="copyMessageScope" + :initial-scope="newStatus.visibility" + :on-scope-change="changeVis" + /> + + <div + v-if="postFormats.length > 1" + class="text-format" + > + <label + for="post-content-type" + class="select" + > + <select + id="post-content-type" + v-model="newStatus.contentType" + class="form-control" + > + <option + v-for="postFormat in postFormats" + :key="postFormat" + :value="postFormat" + > + {{ $t(`post_status.content_type["${postFormat}"]`) }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> <div - v-for="(candidate, index) in candidates" - :key="index" - @click="replace(candidate.utf || (candidate.screen_name + ' '))" - class="autocomplete-item" - :class="{ highlighted: candidate.highlighted }" + v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" + class="text-format" > - <span v-if="candidate.img"><img :src="candidate.img" /></span> - <span v-else>{{candidate.utf}}</span> - <span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span> + <span class="only-format"> + {{ $t(`post_status.content_type["${postFormats[0]}"]`) }} + </span> </div> </div> </div> - <div class='form-bottom'> - <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload> - - <p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p> - <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p> - - <button v-if="posting" disabled class="btn btn-default">{{$t('post_status.posting')}}</button> - <button v-else-if="isOverLengthLimit" disabled class="btn btn-default">{{$t('general.submit')}}</button> - <button v-else :disabled="submitDisabled" type="submit" class="btn btn-default">{{$t('general.submit')}}</button> + <poll-form + v-if="pollsAvailable" + ref="pollForm" + :visible="pollFormVisible" + @update-poll="setPoll" + /> + <div class="form-bottom"> + <div class="form-bottom-left"> + <media-upload + ref="mediaUpload" + :drop-files="dropFiles" + @uploading="disableSubmit" + @uploaded="addMediaFile" + @upload-failed="uploadFailed" + /> + <div + v-if="stickersAvailable" + class="sticker-icon" + > + <i + :title="$t('stickers.add_sticker')" + class="icon-picture btn btn-default" + :class="{ selected: stickerPickerVisible }" + @click="toggleStickerPicker" + /> + </div> + <div + v-if="pollsAvailable" + class="poll-icon" + > + <i + :title="$t('polls.add_poll')" + class="icon-chart-bar btn btn-default" + :class="pollFormVisible && 'selected'" + @click="togglePollForm" + /> + </div> + </div> + <button + v-if="posting" + disabled + class="btn btn-default" + > + {{ $t('post_status.posting') }} + </button> + <button + v-else-if="isOverLengthLimit" + disabled + class="btn btn-default" + > + {{ $t('general.submit') }} + </button> + <button + v-else + :disabled="submitDisabled" + type="submit" + class="btn btn-default" + > + {{ $t('general.submit') }} + </button> </div> - <div class='alert error' v-if="error"> + <div + v-if="error" + class="alert error" + > Error: {{ error }} - <i class="button-icon icon-cancel" @click="clearError"></i> + <i + class="button-icon icon-cancel" + @click="clearError" + /> </div> <div class="attachments"> - <div class="media-upload-wrapper" v-for="file in newStatus.files"> - <i class="fa button-icon icon-cancel" @click="removeMediaFile(file)"></i> + <div + v-for="file in newStatus.files" + :key="file.url" + class="media-upload-wrapper" + > + <i + class="fa button-icon icon-cancel" + @click="removeMediaFile(file)" + /> <div class="media-upload-container attachment"> - <img class="thumbnail media-upload" :src="file.url" v-if="type(file) === 'image'"></img> - <video v-if="type(file) === 'video'" :src="file.url" controls></video> - <audio v-if="type(file) === 'audio'" :src="file.url" controls></audio> - <a v-if="type(file) === 'unknown'" :href="file.url">{{file.url}}</a> + <img + v-if="type(file) === 'image'" + class="thumbnail media-upload" + :src="file.url" + > + <video + v-if="type(file) === 'video'" + :src="file.url" + controls + /> + <audio + v-if="type(file) === 'audio'" + :src="file.url" + controls + /> + <a + v-if="type(file) === 'unknown'" + :href="file.url" + >{{ file.url }}</a> </div> </div> </div> - <div class="upload_settings" v-if="newStatus.files.length > 0"> - <input type="checkbox" id="filesSensitive" v-model="newStatus.nsfw"> - <label for="filesSensitive">{{$t('post_status.attachments_sensitive')}}</label> + <div + v-if="newStatus.files.length > 0" + class="upload_settings" + > + <input + id="filesSensitive" + v-model="newStatus.nsfw" + type="checkbox" + > + <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label> </div> </form> + <sticker-picker + v-if="stickerPickerVisible" + ref="stickerPicker" + @uploaded="addMediaFile" + /> </div> </template> @@ -151,7 +292,6 @@ .visibility-tray { display: flex; justify-content: space-between; - flex-direction: row-reverse; padding-top: 5px; } } @@ -173,6 +313,37 @@ } } + .form-bottom-left { + display: flex; + flex: 1; + } + + .text-format { + .only-format { + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + } + + .poll-icon, .sticker-icon { + font-size: 26px; + flex: 1; + + .selected { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + .sticker-icon { + flex: 0; + min-width: 50px; + } + + .icon-chart-bar { + cursor: pointer; + } + .error { text-align: center; } @@ -233,7 +404,6 @@ } } - .btn { cursor: pointer; } @@ -263,19 +433,38 @@ min-height: 1px; } - form textarea.form-control { - line-height:16px; + .form-post-body { + height: 16px; // Only affects the empty-height + line-height: 16px; resize: none; overflow: hidden; transition: min-height 200ms 100ms; + padding-bottom: 1.75em; min-height: 1px; box-sizing: content-box; } - form textarea.form-control:focus { + .form-post-body:focus { min-height: 48px; } + .main-input { + position: relative; + } + + .character-counter { + position: absolute; + bottom: 0; + right: 0; + padding: 0; + margin: 0 0.5em; + + &.error { + color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); + } + } + .btn { cursor: pointer; } |
