From 0eed2ccca8a0980c161bb5a52b211c507e0ffef5 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 18 Jun 2019 20:28:31 +0000 Subject: Feature/polls attempt 2 --- src/components/media_upload/media_upload.vue | 2 +- src/components/notification/notification.js | 6 +- src/components/notification/notification.vue | 4 +- src/components/poll/poll.js | 107 +++++++++++++++++ src/components/poll/poll.vue | 117 ++++++++++++++++++ src/components/poll/poll_form.js | 121 +++++++++++++++++++ src/components/poll/poll_form.vue | 133 +++++++++++++++++++++ .../post_status_form/post_status_form.js | 44 ++++++- .../post_status_form/post_status_form.vue | 91 +++++++++----- src/components/settings/settings.vue | 2 +- src/components/status/status.js | 12 +- src/components/status/status.vue | 6 +- src/components/timeago/timeago.vue | 48 ++++++++ 13 files changed, 649 insertions(+), 44 deletions(-) create mode 100644 src/components/poll/poll.js create mode 100644 src/components/poll/poll.vue create mode 100644 src/components/poll/poll_form.js create mode 100644 src/components/poll/poll_form.vue create mode 100644 src/components/timeago/timeago.vue (limited to 'src/components') diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index fcdc3471..eb785735 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -13,7 +13,7 @@ diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js new file mode 100644 index 00000000..c0c1ccf7 --- /dev/null +++ b/src/components/poll/poll_form.js @@ -0,0 +1,121 @@ +import * as DateUtils from 'src/services/date_utils/date_utils.js' +import { uniq } from 'lodash' + +export default { + name: 'PollForm', + props: ['visible'], + data: () => ({ + pollType: 'single', + options: ['', ''], + expiryAmount: 10, + expiryUnit: 'minutes' + }), + computed: { + pollLimits () { + return this.$store.state.instance.pollLimits + }, + maxOptions () { + return this.pollLimits.max_options + }, + maxLength () { + return this.pollLimits.max_option_chars + }, + expiryUnits () { + const allUnits = ['minutes', 'hours', 'days'] + const expiry = this.convertExpiryFromUnit + return allUnits.filter( + unit => this.pollLimits.max_expiration >= expiry(unit, 1) + ) + }, + minExpirationInCurrentUnit () { + return Math.ceil( + this.convertExpiryToUnit( + this.expiryUnit, + this.pollLimits.min_expiration + ) + ) + }, + maxExpirationInCurrentUnit () { + return Math.floor( + this.convertExpiryToUnit( + this.expiryUnit, + this.pollLimits.max_expiration + ) + ) + } + }, + methods: { + clear () { + this.pollType = 'single' + this.options = ['', ''] + this.expiryAmount = 10 + this.expiryUnit = 'minutes' + }, + nextOption (index) { + const element = this.$el.querySelector(`#poll-${index + 1}`) + if (element) { + element.focus() + } else { + // Try adding an option and try focusing on it + const addedOption = this.addOption() + if (addedOption) { + this.$nextTick(function () { + this.nextOption(index) + }) + } + } + }, + addOption () { + if (this.options.length < this.maxOptions) { + this.options.push('') + return true + } + return false + }, + deleteOption (index, event) { + if (this.options.length > 2) { + this.options.splice(index, 1) + } + }, + convertExpiryToUnit (unit, amount) { + // Note: we want seconds and not milliseconds + switch (unit) { + case 'minutes': return (1000 * amount) / DateUtils.MINUTE + case 'hours': return (1000 * amount) / DateUtils.HOUR + case 'days': return (1000 * amount) / DateUtils.DAY + } + }, + convertExpiryFromUnit (unit, amount) { + // Note: we want seconds and not milliseconds + switch (unit) { + case 'minutes': return 0.001 * amount * DateUtils.MINUTE + case 'hours': return 0.001 * amount * DateUtils.HOUR + case 'days': return 0.001 * amount * DateUtils.DAY + } + }, + expiryAmountChange () { + this.expiryAmount = + Math.max(this.minExpirationInCurrentUnit, this.expiryAmount) + this.expiryAmount = + Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount) + this.updatePollToParent() + }, + updatePollToParent () { + const expiresIn = this.convertExpiryFromUnit( + this.expiryUnit, + this.expiryAmount + ) + + const options = uniq(this.options.filter(option => option !== '')) + if (options.length < 2) { + this.$emit('update-poll', { error: this.$t('polls.not_enough_options') }) + return + } + this.$emit('update-poll', { + options, + multiple: this.pollType === 'multiple', + expiresIn + }) + } + } +} diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue new file mode 100644 index 00000000..0bc6006d --- /dev/null +++ b/src/components/poll/poll_form.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index a7874acd..5dbb1c9d 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -2,6 +2,7 @@ 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 fileTypeService from '../../services/file_type/file_type.service.js' import { reject, map, uniqBy } from 'lodash' import suggestor from '../emoji-input/suggestor.js' @@ -31,8 +32,9 @@ const PostStatusForm = { ], components: { MediaUpload, - ScopeSelector, - EmojiInput + EmojiInput, + PollForm, + ScopeSelector }, mounted () { this.resize(this.$refs.textarea) @@ -75,10 +77,12 @@ const PostStatusForm = { status: statusText, nsfw: false, files: [], + poll: {}, visibility: scope, contentType }, - caret: 0 + caret: 0, + pollFormVisible: false } }, computed: { @@ -153,8 +157,17 @@ const PostStatusForm = { safeDMEnabled () { return this.$store.state.instance.safeDM }, + 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: { @@ -171,6 +184,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, @@ -180,7 +199,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 = { @@ -188,9 +208,12 @@ const PostStatusForm = { spoilerText: '', files: [], visibility: newStatus.visibility, - contentType: newStatus.contentType + contentType: newStatus.contentType, + poll: {} } + this.pollFormVisible = false this.$refs.mediaUpload.clearFile() + this.clearPollForm() this.$emit('posted') let el = this.$el.querySelector('textarea') el.style.height = 'auto' @@ -261,6 +284,17 @@ const PostStatusForm = { changeVis (visibility) { this.newStatus.visibility = visibility }, + 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 9929aab0..fbeaf39b 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -1,6 +1,6 @@ @@ -172,6 +187,11 @@ } } + .form-bottom-left { + display: flex; + flex: 1; + } + .text-format { .only-format { color: $fallback--faint; @@ -179,6 +199,20 @@ } } + .poll-icon { + font-size: 26px; + flex: 1; + + .selected { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + .icon-chart-bar { + cursor: pointer; + } + .error { text-align: center; @@ -240,7 +274,6 @@ } } - .btn { cursor: pointer; } diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 7067c508..528c13cc 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -302,4 +302,4 @@ + \ No newline at end of file diff --git a/src/components/status/status.js b/src/components/status/status.js index ea4c2b9d..d2452935 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -1,6 +1,7 @@ import Attachment from '../attachment/attachment.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' +import Poll from '../poll/poll.vue' import ExtraButtons from '../extra_buttons/extra_buttons.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCard from '../user_card/user_card.vue' @@ -8,6 +9,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import Gallery from '../gallery/gallery.vue' import LinkPreview from '../link-preview/link-preview.vue' import AvatarList from '../avatar_list/avatar_list.vue' +import Timeago from '../timeago/timeago.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' @@ -216,8 +218,8 @@ const Status = { if (!this.status.summary) return '' const decodedSummary = unescape(this.status.summary) const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined' - ? this.$store.state.instance.subjectLineBehavior - : this.$store.state.config.subjectLineBehavior + ? this.$store.state.instance.subjectLineBehavior + : this.$store.state.config.subjectLineBehavior const startsWithRe = decodedSummary.match(/^re[: ]/i) if (behavior !== 'noop' && startsWithRe || behavior === 'masto') { return decodedSummary @@ -285,11 +287,13 @@ const Status = { RetweetButton, ExtraButtons, PostStatusForm, + Poll, UserCard, UserAvatar, Gallery, LinkPreview, - AvatarList + AvatarList, + Timeago }, methods: { visibilityIcon (visibility) { @@ -377,7 +381,7 @@ const Status = { this.preview = find(statuses, { 'id': targetId }) // or if we have to fetch it if (!this.preview) { - this.$store.state.api.backendInteractor.fetchStatus({id}).then((status) => { + this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => { this.preview = status }) } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index e1dd81ac..58402f7e 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -52,7 +52,7 @@ - +
@@ -123,6 +123,10 @@ {{$t("general.show_less")}}
+
+ +
+
+ + + + \ No newline at end of file -- cgit v1.2.3-70-g09d2