forked from AkkomaGang/akkoma-fe
Feature/polls attempt 2
This commit is contained in:
parent
69eff65130
commit
0eed2ccca8
56 changed files with 1364 additions and 1458 deletions
|
@ -35,7 +35,6 @@
|
|||
"vue-popperjs": "^2.0.3",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vue-timeago": "^3.1.2",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuex": "^3.0.1",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
|
|
47
src/App.scss
47
src/App.scss
|
@ -184,7 +184,43 @@ input, textarea, .select {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
&[type=radio],
|
||||
&[type=radio] {
|
||||
display: none;
|
||||
&:checked + label::before {
|
||||
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
|
||||
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--link, $fallback--link);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
& + label,
|
||||
& + label::before {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
+ label::before {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
transition: box-shadow 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
border-radius: 100%; // Radio buttons should always be circle
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
margin-right: .5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
line-height: 1.1em;
|
||||
font-size: 1.1em;
|
||||
box-sizing: border-box;
|
||||
color: transparent;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
&[type=checkbox] {
|
||||
display: none;
|
||||
&:checked + label::before {
|
||||
|
@ -230,6 +266,15 @@ option {
|
|||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
i[class*=icon-] {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon)
|
||||
|
|
|
@ -215,11 +215,12 @@ const getNodeInfo = async ({ store }) => {
|
|||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
|
||||
const features = metadata.features
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<style>
|
||||
.media-upload {
|
||||
font-size: 26px;
|
||||
flex: 1;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.icon-upload {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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>
|
||||
|
|
107
src/components/poll/poll.js
Normal file
107
src/components/poll/poll.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
import Timeago from '../timeago/timeago.vue'
|
||||
import { forEach, map } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'Poll',
|
||||
props: ['poll', 'statusId'],
|
||||
components: { Timeago },
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
choices: [],
|
||||
refreshInterval: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
|
||||
// Initialize choices to booleans and set its length to match options
|
||||
this.choices = this.poll.options.map(_ => false)
|
||||
},
|
||||
destroyed () {
|
||||
clearTimeout(this.refreshInterval)
|
||||
},
|
||||
computed: {
|
||||
expired () {
|
||||
return Date.now() > Date.parse(this.poll.expires_at)
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
showResults () {
|
||||
return this.poll.voted || this.expired || !this.loggedIn
|
||||
},
|
||||
totalVotesCount () {
|
||||
return this.poll.votes_count
|
||||
},
|
||||
expiresAt () {
|
||||
return Date.parse(this.poll.expires_at).toLocaleString()
|
||||
},
|
||||
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: {
|
||||
refreshPoll () {
|
||||
if (this.expired) return
|
||||
this.fetchPoll()
|
||||
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
|
||||
},
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
117
src/components/poll/poll.vue
Normal file
117
src/components/poll/poll.vue
Normal file
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div class="poll" v-bind:class="containerClass">
|
||||
<div
|
||||
class="poll-option"
|
||||
v-for="(option, index) in poll.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>
|
||||
{{option.title}}
|
||||
</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") }} ·
|
||||
</div>
|
||||
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
|
||||
<Timeago :time="this.poll.expires_at" :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.5em 0;
|
||||
height: 1.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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
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>
|
121
src/components/poll/poll_form.js
Normal file
121
src/components/poll/poll_form.js
Normal file
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
133
src/components/poll/poll_form.vue
Normal file
133
src/components/poll/poll_form.vue
Normal file
|
@ -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>
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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'"
|
||||
|
@ -91,9 +91,24 @@
|
|||
:onScopeChange="changeVis"/>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
|
||||
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
@ -285,11 +287,13 @@ const Status = {
|
|||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
Poll,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview,
|
||||
AvatarList
|
||||
AvatarList,
|
||||
Timeago
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
|
|
@ -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 :poll="status.poll" :status-id="status.id" />
|
||||
</div>
|
||||
|
||||
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
|
||||
<attachment
|
||||
class="non-gallery"
|
||||
|
|
48
src/components/timeago/timeago.vue
Normal file
48
src/components/timeago/timeago.vue
Normal file
|
@ -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>
|
|
@ -168,6 +168,40 @@
|
|||
"true": "sí"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} dia",
|
||||
"days": "{0} dies",
|
||||
"day_short": "{0} dia",
|
||||
"days_short": "{0} dies",
|
||||
"hour": "{0} hour",
|
||||
"hours": "{0} hours",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "in {0}",
|
||||
"in_past": "fa {0}",
|
||||
"minute": "{0} minute",
|
||||
"minutes": "{0} minutes",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} mes",
|
||||
"months": "{0} mesos",
|
||||
"month_short": "{0} mes",
|
||||
"months_short": "{0} mesos",
|
||||
"now": "ara mateix",
|
||||
"now_short": "ara mateix",
|
||||
"second": "{0} second",
|
||||
"seconds": "{0} seconds",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} setm.",
|
||||
"weeks": "{0} setm.",
|
||||
"week_short": "{0} setm.",
|
||||
"weeks_short": "{0} setm.",
|
||||
"year": "{0} any",
|
||||
"years": "{0} anys",
|
||||
"year_short": "{0} any",
|
||||
"years_short": "{0} anys"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Replega",
|
||||
"conversation": "Conversa",
|
||||
|
|
|
@ -350,6 +350,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} day",
|
||||
"days": "{0} days",
|
||||
"day_short": "{0}d",
|
||||
"days_short": "{0}d",
|
||||
"hour": "{0} hour",
|
||||
"hours": "{0} hours",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "in {0}",
|
||||
"in_past": "{0} ago",
|
||||
"minute": "{0} minute",
|
||||
"minutes": "{0} minutes",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} měs",
|
||||
"months": "{0} měs",
|
||||
"month_short": "{0} měs",
|
||||
"months_short": "{0} měs",
|
||||
"now": "teď",
|
||||
"now_short": "teď",
|
||||
"second": "{0} second",
|
||||
"seconds": "{0} seconds",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} týd",
|
||||
"weeks": "{0} týd",
|
||||
"week_short": "{0} týd",
|
||||
"weeks_short": "{0} týd",
|
||||
"year": "{0} r",
|
||||
"years": "{0} l",
|
||||
"year_short": "{0}r",
|
||||
"years_short": "{0}l"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Zabalit",
|
||||
"conversation": "Konverzace",
|
||||
|
|
|
@ -91,6 +91,20 @@
|
|||
"repeated_you": "repeated your status",
|
||||
"no_more_notifications": "No more notifications"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Add Poll",
|
||||
"add_option": "Add Option",
|
||||
"option": "Option",
|
||||
"votes": "votes",
|
||||
"vote": "Vote",
|
||||
"type": "Poll type",
|
||||
"single_choice": "Single choice",
|
||||
"multiple_choices": "Multiple choices",
|
||||
"expiry": "Poll age",
|
||||
"expires_in": "Poll ends in {0}",
|
||||
"expired": "Poll ended {0} ago",
|
||||
"not_enough_options": "Too few unique options in poll"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Repeats and Favorites",
|
||||
"follows": "New follows",
|
||||
|
@ -435,6 +449,40 @@
|
|||
"frontend_version": "Frontend Version"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} day",
|
||||
"days": "{0} days",
|
||||
"day_short": "{0}d",
|
||||
"days_short": "{0}d",
|
||||
"hour": "{0} hour",
|
||||
"hours": "{0} hours",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "in {0}",
|
||||
"in_past": "{0} ago",
|
||||
"minute": "{0} minute",
|
||||
"minutes": "{0} minutes",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} month",
|
||||
"months": "{0} months",
|
||||
"month_short": "{0}mo",
|
||||
"months_short": "{0}mo",
|
||||
"now": "just now",
|
||||
"now_short": "now",
|
||||
"second": "{0} second",
|
||||
"seconds": "{0} seconds",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} week",
|
||||
"weeks": "{0} weeks",
|
||||
"week_short": "{0}w",
|
||||
"weeks_short": "{0}w",
|
||||
"year": "{0} year",
|
||||
"years": "{0} years",
|
||||
"year_short": "{0}y",
|
||||
"years_short": "{0}y"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Collapse",
|
||||
"conversation": "Conversation",
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"chat": "Paikallinen Chat",
|
||||
"friend_requests": "Seurauspyynnöt",
|
||||
"mentions": "Maininnat",
|
||||
"interactions": "Interaktiot",
|
||||
"dms": "Yksityisviestit",
|
||||
"public_tl": "Julkinen Aikajana",
|
||||
"timeline": "Aikajana",
|
||||
|
@ -54,6 +55,25 @@
|
|||
"repeated_you": "toisti viestisi",
|
||||
"no_more_notifications": "Ei enempää ilmoituksia"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Lisää äänestys",
|
||||
"add_option": "Lisää vaihtoehto",
|
||||
"option": "Vaihtoehto",
|
||||
"votes": "ääntä",
|
||||
"vote": "Äänestä",
|
||||
"type": "Äänestyksen tyyppi",
|
||||
"single_choice": "Yksi valinta",
|
||||
"multiple_choices": "Monivalinta",
|
||||
"expiry": "Äänestyksen kesto",
|
||||
"expires_in": "Päättyy {0} päästä",
|
||||
"expired": "Päättyi {0} sitten",
|
||||
"not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Toistot ja tykkäykset",
|
||||
"follows": "Uudet seuraukset",
|
||||
"load_older": "Lataa vanhempia interaktioita"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Uusi viesti",
|
||||
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi",
|
||||
|
@ -210,6 +230,40 @@
|
|||
"true": "päällä"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} päivä",
|
||||
"days": "{0} päivää",
|
||||
"day_short": "{0}pv",
|
||||
"days_short": "{0}pv",
|
||||
"hour": "{0} tunti",
|
||||
"hours": "{0} tuntia",
|
||||
"hour_short": "{0}t",
|
||||
"hours_short": "{0}t",
|
||||
"in_future": "{0} tulevaisuudessa",
|
||||
"in_past": "{0} sitten",
|
||||
"minute": "{0} minuutti",
|
||||
"minutes": "{0} minuuttia",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} kuukausi",
|
||||
"months": "{0} kuukautta",
|
||||
"month_short": "{0}kk",
|
||||
"months_short": "{0}kk",
|
||||
"now": "nyt",
|
||||
"now_short": "juuri nyt",
|
||||
"second": "{0} sekunti",
|
||||
"seconds": "{0} sekuntia",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} viikko",
|
||||
"weeks": "{0} viikkoa",
|
||||
"week_short": "{0}vk",
|
||||
"weeks_short": "{0}vk",
|
||||
"year": "{0} vuosi",
|
||||
"years": "{0} vuotta",
|
||||
"year_short": "{0}v",
|
||||
"years_short": "{0}v"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Sulje",
|
||||
"conversation": "Keskustelu",
|
||||
|
|
|
@ -170,6 +170,40 @@
|
|||
"true": "tá"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} lá",
|
||||
"days": "{0} lá",
|
||||
"day_short": "{0}l",
|
||||
"days_short": "{0}l",
|
||||
"hour": "{0} uair",
|
||||
"hours": "{0} uair",
|
||||
"hour_short": "{0}u",
|
||||
"hours_short": "{0}u",
|
||||
"in_future": "in {0}",
|
||||
"in_past": "{0} ago",
|
||||
"minute": "{0} nóimeád",
|
||||
"minutes": "{0} nóimeád",
|
||||
"minute_short": "{0}n",
|
||||
"minutes_short": "{0}n",
|
||||
"month": "{0} mí",
|
||||
"months": "{0} mí",
|
||||
"month_short": "{0}m",
|
||||
"months_short": "{0}m",
|
||||
"now": "Anois",
|
||||
"now_short": "Anois",
|
||||
"second": "{0} s",
|
||||
"seconds": "{0} s",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} seachtain",
|
||||
"weeks": "{0} seachtaine",
|
||||
"week_short": "{0}se",
|
||||
"weeks_short": "{0}se",
|
||||
"year": "{0} bliainta",
|
||||
"years": "{0} bliainta",
|
||||
"year_short": "{0}b",
|
||||
"years_short": "{0}b"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Folaigh",
|
||||
"conversation": "Cómhra",
|
||||
|
|
|
@ -402,6 +402,40 @@
|
|||
"frontend_version": "フロントエンドのバージョン"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0}日",
|
||||
"days": "{0}日",
|
||||
"day_short": "{0}日",
|
||||
"days_short": "{0}日",
|
||||
"hour": "{0}時間",
|
||||
"hours": "{0}時間",
|
||||
"hour_short": "{0}時間",
|
||||
"hours_short": "{0}時間",
|
||||
"in_future": "{0}で",
|
||||
"in_past": "{0}前",
|
||||
"minute": "{0}分",
|
||||
"minutes": "{0}分",
|
||||
"minute_short": "{0}分",
|
||||
"minutes_short": "{0}分",
|
||||
"month": "{0}ヶ月前",
|
||||
"months": "{0}ヶ月前",
|
||||
"month_short": "{0}ヶ月前",
|
||||
"months_short": "{0}ヶ月前",
|
||||
"now": "たった今",
|
||||
"now_short": "たった今",
|
||||
"second": "{0}秒",
|
||||
"seconds": "{0}秒",
|
||||
"second_short": "{0}秒",
|
||||
"seconds_short": "{0}秒",
|
||||
"week": "{0}週間",
|
||||
"weeks": "{0}週間",
|
||||
"week_short": "{0}週間",
|
||||
"weeks_short": "{0}週間",
|
||||
"year": "{0}年",
|
||||
"years": "{0}年",
|
||||
"year_short": "{0}年",
|
||||
"years_short": "{0}年"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "たたむ",
|
||||
"conversation": "スレッド",
|
||||
|
|
|
@ -402,6 +402,40 @@
|
|||
"frontend_version": "フロントエンドのバージョン"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0}日",
|
||||
"days": "{0}日",
|
||||
"day_short": "{0}日",
|
||||
"days_short": "{0}日",
|
||||
"hour": "{0}時間",
|
||||
"hours": "{0}時間",
|
||||
"hour_short": "{0}時間",
|
||||
"hours_short": "{0}時間",
|
||||
"in_future": "{0}で",
|
||||
"in_past": "{0}前",
|
||||
"minute": "{0}分",
|
||||
"minutes": "{0}分",
|
||||
"minute_short": "{0}分",
|
||||
"minutes_short": "{0}分",
|
||||
"month": "{0}ヶ月前",
|
||||
"months": "{0}ヶ月前",
|
||||
"month_short": "{0}ヶ月前",
|
||||
"months_short": "{0}ヶ月前",
|
||||
"now": "たった今",
|
||||
"now_short": "たった今",
|
||||
"second": "{0}秒",
|
||||
"seconds": "{0}秒",
|
||||
"second_short": "{0}秒",
|
||||
"seconds_short": "{0}秒",
|
||||
"week": "{0}週間",
|
||||
"weeks": "{0}週間",
|
||||
"week_short": "{0}週間",
|
||||
"weeks_short": "{0}週間",
|
||||
"year": "{0}年",
|
||||
"years": "{0}年",
|
||||
"year_short": "{0}年",
|
||||
"years_short": "{0}年"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "たたむ",
|
||||
"conversation": "スレッド",
|
||||
|
|
|
@ -381,6 +381,40 @@
|
|||
"frontend_version": "Version Frontend"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} jorn",
|
||||
"days": "{0} jorns",
|
||||
"day_short": "{0} jorn",
|
||||
"days_short": "{0} jorns",
|
||||
"hour": "{0} hour",
|
||||
"hours": "{0} hours",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "in {0}",
|
||||
"in_past": "fa {0}",
|
||||
"minute": "{0} minute",
|
||||
"minutes": "{0} minutes",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} mes",
|
||||
"months": "{0} meses",
|
||||
"month_short": "{0} mes",
|
||||
"months_short": "{0} meses",
|
||||
"now": "ara meteis",
|
||||
"now_short": "ara meteis",
|
||||
"second": "{0} second",
|
||||
"seconds": "{0} seconds",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} setm.",
|
||||
"weeks": "{0} setm.",
|
||||
"week_short": "{0} setm.",
|
||||
"weeks_short": "{0} setm.",
|
||||
"year": "{0} an",
|
||||
"years": "{0} ans",
|
||||
"year_short": "{0} an",
|
||||
"years_short": "{0} ans"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Tampar",
|
||||
"conversation": "Conversacion",
|
||||
|
|
|
@ -15,7 +15,6 @@ import mediaViewerModule from './modules/media_viewer.js'
|
|||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
|
||||
import VueTimeago from 'vue-timeago'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
import createPersistedState from './lib/persisted_state.js'
|
||||
|
@ -33,14 +32,6 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0]
|
|||
|
||||
Vue.use(Vuex)
|
||||
Vue.use(VueRouter)
|
||||
Vue.use(VueTimeago, {
|
||||
locale: currentLocale === 'cs' ? 'cs' : currentLocale === 'ja' ? 'ja' : 'en',
|
||||
locales: {
|
||||
'cs': require('../static/timeago-cs.json'),
|
||||
'en': require('../static/timeago-en.json'),
|
||||
'ja': require('../static/timeago-ja.json')
|
||||
}
|
||||
})
|
||||
Vue.use(VueI18n)
|
||||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
|
|
|
@ -52,7 +52,15 @@ const defaultState = {
|
|||
|
||||
// Version Information
|
||||
backendVersion: '',
|
||||
frontendVersion: ''
|
||||
frontendVersion: '',
|
||||
|
||||
pollsAvailable: false,
|
||||
pollLimits: {
|
||||
max_options: 4,
|
||||
max_option_chars: 255,
|
||||
min_expiration: 60,
|
||||
max_expiration: 60 * 60 * 24
|
||||
}
|
||||
}
|
||||
|
||||
const instance = {
|
||||
|
|
|
@ -494,6 +494,10 @@ export const mutations = {
|
|||
const newStatus = state.allStatusesObject[id]
|
||||
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
|
||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||
},
|
||||
updateStatusWithPoll (state, { id, poll }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
status.poll = poll
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -578,6 +582,18 @@ const statuses = {
|
|||
]).then(([favoritedByUsers, rebloggedByUsers]) =>
|
||||
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
|
||||
)
|
||||
},
|
||||
votePoll ({ rootState, commit }, { id, pollId, choices }) {
|
||||
return rootState.api.backendInteractor.vote(pollId, choices).then(poll => {
|
||||
commit('updateStatusWithPoll', { id, poll })
|
||||
return poll
|
||||
})
|
||||
},
|
||||
refreshPoll ({ rootState, commit }, { id, pollId }) {
|
||||
return rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
|
||||
commit('updateStatusWithPoll', { id, poll })
|
||||
return poll
|
||||
})
|
||||
}
|
||||
},
|
||||
mutations
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import { each, map, concat, last } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
||||
|
@ -52,6 +57,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
|
|||
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
||||
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
||||
const MASTODON_POLL_URL = id => `/api/v1/polls/${id}`
|
||||
const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by`
|
||||
const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by`
|
||||
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
||||
|
@ -59,11 +66,6 @@ const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
|||
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
|
||||
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
|
||||
|
||||
import { each, map, concat, last } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { StatusCodeError } from '../errors/errors'
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
let fetch = (url, options) => {
|
||||
|
@ -115,8 +117,7 @@ const updateNotificationSettings = ({credentials, settings}) => {
|
|||
headers: authHeaders(credentials),
|
||||
method: 'PUT',
|
||||
body: form
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const updateAvatar = ({ credentials, avatar }) => {
|
||||
|
@ -126,8 +127,7 @@ const updateAvatar = ({credentials, avatar}) => {
|
|||
headers: authHeaders(credentials),
|
||||
method: 'PATCH',
|
||||
body: form
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}).then((data) => data.json())
|
||||
.then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
|
@ -150,8 +150,7 @@ const updateBanner = ({credentials, banner}) => {
|
|||
headers: authHeaders(credentials),
|
||||
method: 'PATCH',
|
||||
body: form
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}).then((data) => data.json())
|
||||
.then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
|
@ -161,8 +160,7 @@ const updateProfile = ({credentials, params}) => {
|
|||
method: 'PATCH',
|
||||
payload: params,
|
||||
credentials
|
||||
})
|
||||
.then((data) => parseUser(data))
|
||||
}).then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
// Params needed:
|
||||
|
@ -459,7 +457,15 @@ const deleteUser = ({credentials, ...user}) => {
|
|||
})
|
||||
}
|
||||
|
||||
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
|
||||
const fetchTimeline = ({
|
||||
timeline,
|
||||
credentials,
|
||||
since = false,
|
||||
until = false,
|
||||
userId = false,
|
||||
tag = false,
|
||||
withMuted = false
|
||||
}) => {
|
||||
const timelineUrls = {
|
||||
public: MASTODON_PUBLIC_TIMELINE,
|
||||
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||
|
@ -558,8 +564,19 @@ const unretweet = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
|
||||
const postStatus = ({
|
||||
credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds = [],
|
||||
inReplyToStatusId,
|
||||
contentType
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
const pollOptions = poll.options || []
|
||||
|
||||
form.append('status', status)
|
||||
form.append('source', 'Pleroma FE')
|
||||
|
@ -570,6 +587,19 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
|
|||
mediaIds.forEach(val => {
|
||||
form.append('media_ids[]', val)
|
||||
})
|
||||
if (pollOptions.some(option => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: poll.expiresIn,
|
||||
multiple: poll.multiple
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach(key => {
|
||||
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||
})
|
||||
|
||||
pollOptions.forEach(option => {
|
||||
form.append('poll[options][]', option)
|
||||
})
|
||||
}
|
||||
if (inReplyToStatusId) {
|
||||
form.append('in_reply_to_id', inReplyToStatusId)
|
||||
}
|
||||
|
@ -761,6 +791,30 @@ const markNotificationsAsSeen = ({id, credentials}) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const vote = ({ pollId, choices, credentials }) => {
|
||||
const form = new FormData()
|
||||
form.append('choices', choices)
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload: {
|
||||
choices: choices
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPoll = ({ pollId, credentials }) => {
|
||||
return promisedRequest(
|
||||
{
|
||||
url: MASTODON_POLL_URL(encodeURIComponent(pollId)),
|
||||
method: 'GET',
|
||||
credentials
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const fetchFavoritedByUsers = ({ id }) => {
|
||||
return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
|
||||
}
|
||||
|
@ -840,6 +894,8 @@ const apiService = {
|
|||
denyUser,
|
||||
suggestions,
|
||||
markNotificationsAsSeen,
|
||||
vote,
|
||||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
reportUser,
|
||||
|
|
|
@ -2,7 +2,7 @@ import apiService from '../api/api.service.js'
|
|||
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||
|
||||
const backendInteractorService = (credentials) => {
|
||||
const backendInteractorService = credentials => {
|
||||
const fetchStatus = ({ id }) => {
|
||||
return apiService.fetchStatus({ id, credentials })
|
||||
}
|
||||
|
@ -87,6 +87,14 @@ const backendInteractorService = (credentials) => {
|
|||
return apiService.deleteUser({ screen_name, credentials })
|
||||
}
|
||||
|
||||
const vote = (pollId, choices) => {
|
||||
return apiService.vote({ credentials, pollId, choices })
|
||||
}
|
||||
|
||||
const fetchPoll = (pollId) => {
|
||||
return apiService.fetchPoll({ credentials, pollId })
|
||||
}
|
||||
|
||||
const updateNotificationSettings = ({ settings }) => {
|
||||
return apiService.updateNotificationSettings({ credentials, settings })
|
||||
}
|
||||
|
@ -110,11 +118,13 @@ const backendInteractorService = (credentials) => {
|
|||
const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
|
||||
|
||||
const externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials })
|
||||
|
||||
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
|
||||
const importFollows = (file) => apiService.importFollows({ file, credentials })
|
||||
|
||||
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
|
||||
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
|
||||
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
|
||||
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
|
||||
|
||||
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
|
||||
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
|
||||
|
@ -180,6 +190,8 @@ const backendInteractorService = (credentials) => {
|
|||
fetchFollowRequests,
|
||||
approveUser,
|
||||
denyUser,
|
||||
vote,
|
||||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
reportUser,
|
||||
|
|
45
src/services/date_utils/date_utils.js
Normal file
45
src/services/date_utils/date_utils.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
export const SECOND = 1000
|
||||
export const MINUTE = 60 * SECOND
|
||||
export const HOUR = 60 * MINUTE
|
||||
export const DAY = 24 * HOUR
|
||||
export const WEEK = 7 * DAY
|
||||
export const MONTH = 30 * DAY
|
||||
export const YEAR = 365.25 * DAY
|
||||
|
||||
export const relativeTime = (date, nowThreshold = 1) => {
|
||||
if (typeof date === 'string') date = Date.parse(date)
|
||||
const round = Date.now() > date ? Math.floor : Math.ceil
|
||||
const d = Math.abs(Date.now() - date)
|
||||
let r = { num: round(d / YEAR), key: 'time.years' }
|
||||
if (d < nowThreshold * SECOND) {
|
||||
r.num = 0
|
||||
r.key = 'time.now'
|
||||
} else if (d < MINUTE) {
|
||||
r.num = round(d / SECOND)
|
||||
r.key = 'time.seconds'
|
||||
} else if (d < HOUR) {
|
||||
r.num = round(d / MINUTE)
|
||||
r.key = 'time.minutes'
|
||||
} else if (d < DAY) {
|
||||
r.num = round(d / HOUR)
|
||||
r.key = 'time.hours'
|
||||
} else if (d < WEEK) {
|
||||
r.num = round(d / DAY)
|
||||
r.key = 'time.days'
|
||||
} else if (d < MONTH) {
|
||||
r.num = round(d / WEEK)
|
||||
r.key = 'time.weeks'
|
||||
} else if (d < YEAR) {
|
||||
r.num = round(d / MONTH)
|
||||
r.key = 'time.months'
|
||||
}
|
||||
// Remove plural form when singular
|
||||
if (r.num === 1) r.key = r.key.slice(0, -1)
|
||||
return r
|
||||
}
|
||||
|
||||
export const relativeTimeShort = (date, nowThreshold = 1) => {
|
||||
const r = relativeTime(date, nowThreshold)
|
||||
r.key += '_short'
|
||||
return r
|
||||
}
|
|
@ -234,6 +234,7 @@ export const parseStatus = (data) => {
|
|||
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
output.pinned = data.pinned
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import { map } from 'lodash'
|
||||
import apiService from '../api/api.service.js'
|
||||
|
||||
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
|
||||
const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
|
||||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType})
|
||||
return apiService.postStatus({
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
sensitive,
|
||||
mediaIds,
|
||||
inReplyToStatusId,
|
||||
contentType,
|
||||
poll})
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
store.dispatch('addNewStatuses', {
|
||||
|
|
|
@ -202,6 +202,7 @@ const generateColors = (input) => {
|
|||
colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
|
||||
|
||||
colors.faintLink = col.faintLink || Object.assign({}, col.link)
|
||||
colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
|
||||
|
||||
colors.icon = mixrgb(colors.bg, colors.text)
|
||||
|
||||
|
|
0
static/font/LICENSE.txt
Executable file → Normal file
0
static/font/LICENSE.txt
Executable file → Normal file
0
static/font/README.txt
Executable file → Normal file
0
static/font/README.txt
Executable file → Normal file
6
static/font/config.json
Executable file → Normal file
6
static/font/config.json
Executable file → Normal file
|
@ -240,6 +240,12 @@
|
|||
"code": 59416,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "266d5d9adf15a61800477a5acf9a4462",
|
||||
"css": "chart-bar",
|
||||
"code": 59419,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "671f29fa10dda08074a4c6a341bb4f39",
|
||||
"css": "bell-alt",
|
||||
|
|
1
static/font/css/fontello-codes.css
vendored
1
static/font/css/fontello-codes.css
vendored
|
@ -26,6 +26,7 @@
|
|||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-pin:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-chart-bar:before { content: '\e81b'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
13
static/font/css/fontello-embedded.css
vendored
13
static/font/css/fontello-embedded.css
vendored
File diff suppressed because one or more lines are too long
1
static/font/css/fontello-ie7-codes.css
vendored
1
static/font/css/fontello-ie7-codes.css
vendored
|
@ -26,6 +26,7 @@
|
|||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
1
static/font/css/fontello-ie7.css
vendored
1
static/font/css/fontello-ie7.css
vendored
|
@ -37,6 +37,7 @@
|
|||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
15
static/font/css/fontello.css
vendored
15
static/font/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?16609299');
|
||||
src: url('../font/fontello.eot?16609299#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?16609299') format('woff2'),
|
||||
url('../font/fontello.woff?16609299') format('woff'),
|
||||
url('../font/fontello.ttf?16609299') format('truetype'),
|
||||
url('../font/fontello.svg?16609299#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?3304725');
|
||||
src: url('../font/fontello.eot?3304725#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?3304725') format('woff2'),
|
||||
url('../font/fontello.woff?3304725') format('woff'),
|
||||
url('../font/fontello.ttf?3304725') format('truetype'),
|
||||
url('../font/fontello.svg?3304725#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?16609299#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?3304725#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -82,6 +82,7 @@
|
|||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-pin:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-chart-bar:before { content: '\e81b'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
19
static/font/demo.html
Executable file → Normal file
19
static/font/demo.html
Executable file → Normal file
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('./font/fontello.eot?79958594');
|
||||
src: url('./font/fontello.eot?79958594#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?79958594') format('woff'),
|
||||
url('./font/fontello.ttf?79958594') format('truetype'),
|
||||
url('./font/fontello.svg?79958594#fontello') format('svg');
|
||||
src: url('./font/fontello.eot?14310629');
|
||||
src: url('./font/fontello.eot?14310629#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?14310629') format('woff'),
|
||||
url('./font/fontello.ttf?14310629') format('truetype'),
|
||||
url('./font/fontello.svg?14310629#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -337,27 +337,28 @@ body {
|
|||
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil"></i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-pin"></i> <span class="i-name">icon-pin</span><span class="i-code">0xe819</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-wrench"></i> <span class="i-name">icon-wrench</span><span class="i-code">0xe81a</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar"></i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt"></i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis"></i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
|
||||
|
|
Binary file not shown.
|
@ -60,6 +60,8 @@
|
|||
|
||||
<glyph glyph-name="wrench" unicode="" d="M214 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="chart-bar" unicode="" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
|
||||
|
||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
||||
|
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"ara mateix",
|
||||
["fa %s s", "fa %s s"],
|
||||
["fa %s min", "fa %s min"],
|
||||
["fa %s h", "fa %s h"],
|
||||
["fa %s dia", "fa %s dies"],
|
||||
["fa %s setm.", "fa %s setm."],
|
||||
["fa %s mes", "fa %s mesos"],
|
||||
["fa %s any", "fa %s anys"]
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"teď",
|
||||
["%s s", "%s s"],
|
||||
["%s min", "%s min"],
|
||||
["%s h", "%s h"],
|
||||
["%s d", "%s d"],
|
||||
["%s týd", "%s týd"],
|
||||
["%s měs", "%s měs"],
|
||||
["%s r", "%s l"]
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"now",
|
||||
["%ss", "%ss"],
|
||||
["%smin", "%smin"],
|
||||
["%sh", "%sh"],
|
||||
["%sd", "%sd"],
|
||||
["%sw", "%sw"],
|
||||
["%smo", "%smo"],
|
||||
["%sy", "%sy"]
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"Anois",
|
||||
["%s s", "%s s"],
|
||||
["%s n", "%s nóimeád"],
|
||||
["%s u", "%s uair"],
|
||||
["%s l", "%s lá"],
|
||||
["%s se", "%s seachtaine"],
|
||||
["%s m", "%s mí"],
|
||||
["%s b", "%s bliainta"]
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"たった今",
|
||||
"%s 秒前",
|
||||
"%s 分前",
|
||||
"%s 時間前",
|
||||
"%s 日前",
|
||||
"%s 週間前",
|
||||
"%s ヶ月前",
|
||||
"%s 年前"
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
"ara meteis",
|
||||
["fa %s s", "fa %s s"],
|
||||
["fa %s min", "fa %s min"],
|
||||
["fa %s h", "fa %s h"],
|
||||
["fa %s jorn", "fa %s jorns"],
|
||||
["fa %s setm.", "fa %s setm."],
|
||||
["fa %s mes", "fa %s meses"],
|
||||
["fa %s an", "fa %s ans"]
|
||||
]
|
40
test/unit/specs/services/date_utils/date_utils.spec.js
Normal file
40
test/unit/specs/services/date_utils/date_utils.spec.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
||||
|
||||
describe('DateUtils', () => {
|
||||
describe('relativeTime', () => {
|
||||
it('returns now with low enough amount of seconds', () => {
|
||||
const futureTime = Date.now() + 20 * DateUtils.SECOND
|
||||
const pastTime = Date.now() - 20 * DateUtils.SECOND
|
||||
expect(DateUtils.relativeTime(futureTime, 30)).to.eql({ num: 0, key: 'time.now' })
|
||||
expect(DateUtils.relativeTime(pastTime, 30)).to.eql({ num: 0, key: 'time.now' })
|
||||
})
|
||||
|
||||
it('rounds down for past', () => {
|
||||
const time = Date.now() - 1.8 * DateUtils.HOUR
|
||||
expect(DateUtils.relativeTime(time)).to.eql({ num: 1, key: 'time.hour' })
|
||||
})
|
||||
|
||||
it('rounds up for future', () => {
|
||||
const time = Date.now() + 1.8 * DateUtils.HOUR
|
||||
expect(DateUtils.relativeTime(time)).to.eql({ num: 2, key: 'time.hours' })
|
||||
})
|
||||
|
||||
it('uses plural when necessary', () => {
|
||||
const time = Date.now() - 3.8 * DateUtils.WEEK
|
||||
expect(DateUtils.relativeTime(time)).to.eql({ num: 3, key: 'time.weeks' })
|
||||
})
|
||||
|
||||
it('works with date string', () => {
|
||||
const time = Date.now() - 4 * DateUtils.MONTH
|
||||
const dateString = new Date(time).toISOString()
|
||||
expect(DateUtils.relativeTime(dateString)).to.eql({ num: 4, key: 'time.months' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('relativeTimeShort', () => {
|
||||
it('returns the short version of the same relative time', () => {
|
||||
const time = Date.now() + 2 * DateUtils.YEAR
|
||||
expect(DateUtils.relativeTimeShort(time)).to.eql({ num: 2, key: 'time.years_short' })
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue