aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/emoji-input/emoji-input.js21
-rw-r--r--src/components/emoji-input/emoji-input.vue2
-rw-r--r--src/components/gallery/gallery.vue4
-rw-r--r--src/components/login_form/login_form.js3
-rw-r--r--src/components/media_upload/media_upload.vue2
-rw-r--r--src/components/notification/notification.js6
-rw-r--r--src/components/notification/notification.vue4
-rw-r--r--src/components/oauth_callback/oauth_callback.js3
-rw-r--r--src/components/poll/poll.js112
-rw-r--r--src/components/poll/poll.vue121
-rw-r--r--src/components/poll/poll_form.js121
-rw-r--r--src/components/poll/poll_form.vue133
-rw-r--r--src/components/post_status_form/post_status_form.js51
-rw-r--r--src/components/post_status_form/post_status_form.vue119
-rw-r--r--src/components/settings/settings.vue2
-rw-r--r--src/components/status/status.js12
-rw-r--r--src/components/status/status.vue6
-rw-r--r--src/components/timeago/timeago.vue48
-rw-r--r--src/components/user_profile/user_profile.vue2
19 files changed, 713 insertions, 59 deletions
diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js
index fde99320..b09dc628 100644
--- a/src/components/emoji-input/emoji-input.js
+++ b/src/components/emoji-input/emoji-input.js
@@ -59,7 +59,8 @@ const EmojiInput = {
input: undefined,
highlighted: 0,
caret: 0,
- focused: false
+ focused: false,
+ blurTimeout: null
}
},
computed: {
@@ -124,12 +125,12 @@ const EmojiInput = {
this.$emit('input', newValue)
this.caret = 0
},
- replaceText (e) {
+ replaceText (e, suggestion) {
const len = this.suggestions.length || 0
if (this.textAtCaret.length === 1) { return }
- if (len > 0) {
- const suggestion = this.suggestions[this.highlighted]
- const replacement = suggestion.replacement
+ if (len > 0 || suggestion) {
+ const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
+ const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('input', newValue)
this.highlighted = 0
@@ -175,13 +176,21 @@ const EmojiInput = {
onBlur (e) {
// Clicking on any suggestion removes focus from autocomplete,
// preventing click handler ever executing.
- setTimeout(() => {
+ this.blurTimeout = setTimeout(() => {
this.focused = false
this.setCaret(e)
this.resize()
}, 200)
},
+ onClick (e, suggestion) {
+ this.replaceText(e, suggestion)
+ },
onFocus (e) {
+ if (this.blurTimeout) {
+ clearTimeout(this.blurTimeout)
+ this.blurTimeout = null
+ }
+
this.focused = true
this.setCaret(e)
this.resize()
diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue
index 8437a495..b1f7afa5 100644
--- a/src/components/emoji-input/emoji-input.vue
+++ b/src/components/emoji-input/emoji-input.vue
@@ -6,7 +6,7 @@
<div
v-for="(suggestion, index) in suggestions"
:key="index"
- @click.stop.prevent="replaceText"
+ @click.stop.prevent="onClick($event, suggestion)"
class="autocomplete-item"
:class="{ highlighted: suggestion.highlighted }"
>
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index ea525c95..f0433f10 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -28,7 +28,9 @@
flex-grow: 1;
margin-top: 0.5em;
- .attachments, .attachment {
+ // FIXME: specificity problem with this and .attachments.attachment
+ // we shouldn't have the need for .image here
+ .attachment.image {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 93214646..4a5b1965 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -26,9 +26,10 @@ const LoginForm = {
this.isTokenAuth ? this.submitToken() : this.submitPassword()
},
submitToken () {
- const { clientId } = this.oauth
+ const { clientId, clientSecret } = this.oauth
const data = {
clientId,
+ clientSecret,
instance: this.instance.server,
commit: this.$store.commit
}
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 @@
<style>
.media-upload {
font-size: 26px;
- flex: 1;
+ min-width: 50px;
}
.icon-upload {
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index e59e7497..896c6d52 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
+import Timeago from '../timeago/timeago.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -13,7 +14,10 @@ const Notification = {
},
props: [ 'notification' ],
components: {
- Status, UserAvatar, UserCard
+ Status,
+ UserAvatar,
+ UserCard,
+ Timeago
},
methods: {
toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 3427b9c5..5ad365ad 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -30,12 +30,12 @@
</div>
<div class="timeago" v-if="notification.type === 'follow'">
<span class="faint">
- <timeago :since="notification.created_at" :auto-update="240"></timeago>
+ <Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</span>
</div>
<div class="timeago" v-else>
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
- <timeago :since="notification.created_at" :auto-update="240"></timeago>
+ <Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</router-link>
</div>
</span>
diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js
index 2c6ca235..a3c7b7f9 100644
--- a/src/components/oauth_callback/oauth_callback.js
+++ b/src/components/oauth_callback/oauth_callback.js
@@ -4,10 +4,11 @@ const oac = {
props: ['code'],
mounted () {
if (this.code) {
- const { clientId } = this.$store.state.oauth
+ const { clientId, clientSecret } = this.$store.state.oauth
oauth.getToken({
clientId,
+ clientSecret,
instance: this.$store.state.instance.server,
code: this.code
}).then((result) => {
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
new file mode 100644
index 00000000..98db5582
--- /dev/null
+++ b/src/components/poll/poll.js
@@ -0,0 +1,112 @@
+import Timeago from '../timeago/timeago.vue'
+import { forEach, map } from 'lodash'
+
+export default {
+ name: 'Poll',
+ props: ['basePoll'],
+ components: { Timeago },
+ data () {
+ return {
+ loading: false,
+ choices: []
+ }
+ },
+ created () {
+ if (!this.$store.state.polls.pollsObject[this.pollId]) {
+ this.$store.dispatch('mergeOrAddPoll', this.basePoll)
+ }
+ this.$store.dispatch('trackPoll', this.pollId)
+ },
+ destroyed () {
+ this.$store.dispatch('untrackPoll', this.pollId)
+ },
+ computed: {
+ pollId () {
+ return this.basePoll.id
+ },
+ poll () {
+ const storePoll = this.$store.state.polls.pollsObject[this.pollId]
+ return storePoll || {}
+ },
+ options () {
+ return (this.poll && this.poll.options) || []
+ },
+ expiresAt () {
+ return (this.poll && this.poll.expires_at) || 0
+ },
+ expired () {
+ return (this.poll && this.poll.expired) || false
+ },
+ loggedIn () {
+ return this.$store.state.users.currentUser
+ },
+ showResults () {
+ return this.poll.voted || this.expired || !this.loggedIn
+ },
+ totalVotesCount () {
+ return this.poll.votes_count
+ },
+ containerClass () {
+ return {
+ loading: this.loading
+ }
+ },
+ choiceIndices () {
+ // Convert array of booleans into an array of indices of the
+ // items that were 'true', so [true, false, false, true] becomes
+ // [0, 3].
+ return this.choices
+ .map((entry, index) => entry && index)
+ .filter(value => typeof value === 'number')
+ },
+ isDisabled () {
+ const noChoice = this.choiceIndices.length === 0
+ return this.loading || noChoice
+ }
+ },
+ methods: {
+ percentageForOption (count) {
+ return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
+ },
+ resultTitle (option) {
+ return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
+ },
+ fetchPoll () {
+ this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
+ },
+ activateOption (index) {
+ // forgive me father: doing checking the radio/checkboxes
+ // in code because of customized input elements need either
+ // a) an extra element for the actual graphic, or b) use a
+ // pseudo element for the label. We use b) which mandates
+ // using "for" and "id" matching which isn't nice when the
+ // same poll appears multiple times on the site (notifs and
+ // timeline for example). With code we can make sure it just
+ // works without altering the pseudo element implementation.
+ const allElements = this.$el.querySelectorAll('input')
+ const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
+ if (this.poll.multiple) {
+ // Checkboxes, toggle only the clicked one
+ clickedElement.checked = !clickedElement.checked
+ } else {
+ // Radio button, uncheck everything and check the clicked one
+ forEach(allElements, element => { element.checked = false })
+ clickedElement.checked = true
+ }
+ this.choices = map(allElements, e => e.checked)
+ },
+ optionId (index) {
+ return `poll${this.poll.id}-${index}`
+ },
+ vote () {
+ if (this.choiceIndices.length === 0) return
+ this.loading = true
+ this.$store.dispatch(
+ 'votePoll',
+ { id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
+ ).then(poll => {
+ this.loading = false
+ })
+ }
+ }
+}
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
new file mode 100644
index 00000000..bb67101a
--- /dev/null
+++ b/src/components/poll/poll.vue
@@ -0,0 +1,121 @@
+<template>
+ <div class="poll" v-bind:class="containerClass">
+ <div
+ class="poll-option"
+ v-for="(option, index) in options"
+ :key="index"
+ >
+ <div v-if="showResults" :title="resultTitle(option)" class="option-result">
+ <div class="option-result-label">
+ <span class="result-percentage">
+ {{percentageForOption(option.votes_count)}}%
+ </span>
+ <span>{{option.title}}</span>
+ </div>
+ <div
+ class="result-fill"
+ :style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
+ >
+ </div>
+ </div>
+ <div v-else @click="activateOption(index)">
+ <input
+ v-if="poll.multiple"
+ type="checkbox"
+ :disabled="loading"
+ :value="index"
+ >
+ <input
+ v-else
+ type="radio"
+ :disabled="loading"
+ :value="index"
+ >
+ <label class="option-vote">
+ <div>{{option.title}}</div>
+ </label>
+ </div>
+ </div>
+ <div class="footer faint">
+ <button
+ v-if="!showResults"
+ class="btn btn-default poll-vote-button"
+ type="button"
+ @click="vote"
+ :disabled="isDisabled"
+ >
+ {{$t('polls.vote')}}
+ </button>
+ <div class="total">
+ {{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;ยท&nbsp;
+ </div>
+ <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
+ <Timeago :time="this.expiresAt" :auto-update="60" :now-threshold="0" />
+ </i18n>
+ </div>
+ </div>
+</template>
+
+<script src="./poll.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.poll {
+ .votes {
+ display: flex;
+ flex-direction: column;
+ margin: 0 0 0.5em;
+ }
+ .poll-option {
+ margin: 0.75em 0.5em;
+ }
+ .option-result {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ .option-result-label {
+ display: flex;
+ align-items: center;
+ padding: 0.1em 0.25em;
+ z-index: 1;
+ }
+ .result-percentage {
+ width: 3.5em;
+ flex-shrink: 0;
+ }
+ .result-fill {
+ height: 100%;
+ position: absolute;
+ background-color: $fallback--lightBg;
+ background-color: var(--linkBg, $fallback--lightBg);
+ border-radius: $fallback--panelRadius;
+ border-radius: var(--panelRadius, $fallback--panelRadius);
+ top: 0;
+ left: 0;
+ transition: width 0.5s;
+ }
+ .option-vote {
+ display: flex;
+ align-items: center;
+ }
+ input {
+ width: 3.5em;
+ }
+ .footer {
+ display: flex;
+ align-items: center;
+ }
+ &.loading * {
+ cursor: progress;
+ }
+ .poll-vote-button {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ }
+}
+</style>
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 @@
+<template>
+ <div class="poll-form" v-if="visible">
+ <div class="poll-option" v-for="(option, index) in options" :key="index">
+ <div class="input-container">
+ <input
+ class="poll-option-input"
+ type="text"
+ :placeholder="$t('polls.option')"
+ :maxlength="maxLength"
+ :id="`poll-${index}`"
+ v-model="options[index]"
+ @change="updatePollToParent"
+ @keydown.enter.stop.prevent="nextOption(index)"
+ >
+ </div>
+ <div class="icon-container" v-if="options.length > 2">
+ <i class="icon-cancel" @click="deleteOption(index)"></i>
+ </div>
+ </div>
+ <a
+ v-if="options.length < maxOptions"
+ class="add-option faint"
+ @click="addOption"
+ >
+ <i class="icon-plus" />
+ {{ $t("polls.add_option") }}
+ </a>
+ <div class="poll-type-expiry">
+ <div class="poll-type" :title="$t('polls.type')">
+ <label for="poll-type-selector" class="select">
+ <select class="select" v-model="pollType" @change="updatePollToParent">
+ <option value="single">{{$t('polls.single_choice')}}</option>
+ <option value="multiple">{{$t('polls.multiple_choices')}}</option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ <div class="poll-expiry" :title="$t('polls.expiry')">
+ <input
+ type="number"
+ class="expiry-amount hide-number-spinner"
+ :min="minExpirationInCurrentUnit"
+ :max="maxExpirationInCurrentUnit"
+ v-model="expiryAmount"
+ @change="expiryAmountChange"
+ >
+ <label class="expiry-unit select">
+ <select
+ v-model="expiryUnit"
+ @change="expiryAmountChange"
+ >
+ <option v-for="unit in expiryUnits" :value="unit">
+ {{ $t(`time.${unit}_short`, ['']) }}
+ </option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./poll_form.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.poll-form {
+ display: flex;
+ flex-direction: column;
+ padding: 0 0.5em 0.5em;
+
+ .add-option {
+ align-self: flex-start;
+ padding-top: 0.25em;
+ cursor: pointer;
+ }
+
+ .poll-option {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 0.25em;
+ }
+
+ .input-container {
+ width: 100%;
+ input {
+ // Hack: dodge the floating X icon
+ padding-right: 2.5em;
+ width: 100%;
+ }
+ }
+
+ .icon-container {
+ // Hack: Move the icon over the input box
+ width: 2em;
+ margin-left: -2em;
+ z-index: 1;
+ }
+
+ .poll-type-expiry {
+ margin-top: 0.5em;
+ display: flex;
+ width: 100%;
+ }
+
+ .poll-type {
+ margin-right: 0.75em;
+ flex: 1 1 60%;
+ .select {
+ border: none;
+ box-shadow: none;
+ background-color: transparent;
+ }
+ }
+
+ .poll-expiry {
+ display: flex;
+
+ .expiry-amount {
+ width: 3em;
+ text-align: right;
+ }
+
+ .expiry-unit {
+ border: none;
+ box-shadow: none;
+ background-color: transparent;
+ }
+ }
+}
+</style>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index a7874acd..ef6b0fce 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'
@@ -246,8 +269,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`
@@ -261,6 +287,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..67cdc721 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 @@
<template>
<div class="post-status-form">
- <form @submit.prevent="postStatus(newStatus)">
+ <form @submit.prevent="postStatus(newStatus)" autocomplete="off">
<div class="form-group" >
<i18n
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
@@ -48,7 +48,7 @@
<EmojiInput
:suggest="emojiUserSuggestor"
v-model="newStatus.status"
- class="form-control"
+ class="form-control main-input"
>
<textarea
ref="textarea"
@@ -65,6 +65,13 @@
class="form-post-body"
>
</textarea>
+ <p
+ v-if="hasStatusLengthLimit"
+ class="character-counter faint"
+ :class="{ error: isOverLengthLimit }"
+ >
+ {{ charactersLeft }}
+ </p>
</EmojiInput>
<div class="visibility-tray">
<div class="text-format" v-if="postFormats.length > 1">
@@ -91,37 +98,50 @@
:onScopeChange="changeVis"/>
</div>
</div>
- <div class='form-bottom'>
+ <poll-form
+ ref="pollForm"
+ v-if="pollsAvailable"
+ :visible="pollFormVisible"
+ @update-poll="setPoll"
+ />
+ <div class='form-bottom'>
+ <div class='form-bottom-left'>
<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>
- </div>
- <div class='alert error' v-if="error">
- Error: {{ error }}
- <i class="button-icon icon-cancel" @click="clearError"></i>
- </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 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>
- </div>
+ <div v-if="pollsAvailable" class="poll-icon">
+ <i
+ :title="$t('polls.add_poll')"
+ @click="togglePollForm"
+ class="icon-chart-bar btn btn-default"
+ :class="pollFormVisible && 'selected'"
+ />
</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>
+
+ <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">
+ Error: {{ error }}
+ <i class="button-icon icon-cancel" @click="clearError"></i>
+ </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 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>
+ </div>
</div>
- </form>
- </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>
+ </form>
+</div>
</template>
<script src="./post_status_form.js"></script>
@@ -172,6 +192,11 @@
}
}
+ .form-bottom-left {
+ display: flex;
+ flex: 1;
+ }
+
.text-format {
.only-format {
color: $fallback--faint;
@@ -179,6 +204,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 +279,6 @@
}
}
-
.btn {
cursor: pointer;
}
@@ -271,10 +309,12 @@
}
.form-post-body {
- line-height:16px;
+ 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;
}
@@ -283,6 +323,23 @@
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;
}
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 @@
</template>
<script src="./settings.js">
-</script>
+</script> \ 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..440e1957 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -52,7 +52,7 @@
<span class="heading-right">
<router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
- <timeago :since="status.created_at" :auto-update="60"></timeago>
+ <Timeago :time="status.created_at" :auto-update="60"></Timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
@@ -123,6 +123,10 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
</div>
+ <div v-if="status.poll && status.poll.options">
+ <poll :base-poll="status.poll" />
+ </div>
+
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
<attachment
class="non-gallery"
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
new file mode 100644
index 00000000..3f185a2d
--- /dev/null
+++ b/src/components/timeago/timeago.vue
@@ -0,0 +1,48 @@
+<template>
+ <time :datetime="time" :title="localeDateString">
+ {{ $t(relativeTime.key, [relativeTime.num]) }}
+ </time>
+</template>
+
+<script>
+import * as DateUtils from 'src/services/date_utils/date_utils.js'
+
+export default {
+ name: 'Timeago',
+ props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
+ data () {
+ return {
+ relativeTime: { key: 'time.now', num: 0 },
+ interval: null
+ }
+ },
+ created () {
+ this.refreshRelativeTimeObject()
+ },
+ destroyed () {
+ clearTimeout(this.interval)
+ },
+ computed: {
+ localeDateString () {
+ return typeof this.time === 'string'
+ ? new Date(Date.parse(this.time)).toLocaleString()
+ : this.time.toLocaleString()
+ }
+ },
+ methods: {
+ refreshRelativeTimeObject () {
+ const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
+ this.relativeTime = this.longFormat
+ ? DateUtils.relativeTime(this.time, nowThreshold)
+ : DateUtils.relativeTimeShort(this.time, nowThreshold)
+
+ if (this.autoUpdate) {
+ this.interval = setTimeout(
+ this.refreshRelativeTimeObject,
+ 1000 * this.autoUpdate
+ )
+ }
+ }
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 48b774ea..84a159da 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -3,7 +3,7 @@
<div v-if="user" class="user-profile panel panel-default">
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
- <div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
+ <div :label="$t('user_card.statuses')">
<div class="timeline">
<template v-for="statusId in user.pinnedStatuseIds">
<Conversation