Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop:
  Feature/polls attempt 2
  Update translation with review comments
  Update file with current en.json
  Update fr.json added missing ,
  Update fr.json stoped at line 134, more to do below
This commit is contained in:
Henry Jameson 2019-06-18 23:45:28 +03:00
commit 9f8b0ce5e1
57 changed files with 1909 additions and 1663 deletions

View file

@ -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"

View file

@ -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)

View file

@ -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 })

View file

@ -13,7 +13,7 @@
<style>
.media-upload {
font-size: 26px;
flex: 1;
min-width: 50px;
}
.icon-upload {

View file

@ -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 () {

View file

@ -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
View 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
})
}
}
}

View 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") }}&nbsp;·&nbsp;
</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>

View 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
})
}
}
}

View 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>

View file

@ -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 })
}

View file

@ -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,37 +91,52 @@
: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>
<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>
</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 +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;
}

View file

@ -302,4 +302,4 @@
</template>
<script src="./settings.js">
</script>
</script>

View file

@ -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
})
}

View file

@ -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"

View 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>

View file

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

View file

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

View file

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

View file

@ -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",
@ -264,9 +318,9 @@
},
"upload":{
"error": {
"base": "Lataus epäonnistui.",
"file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Yritä uudestaan myöhemmin"
"base": "Lataus epäonnistui.",
"file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Yritä uudestaan myöhemmin"
},
"file_size_units": {
"B": "tavua",

View file

@ -1,209 +1,549 @@
{
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Proxy média",
"scope_options": "Options de visibilité",
"text_limit": "Limite du texte",
"title": "Caractéristiques",
"who_to_follow": "Qui s'abonner"
},
"finder": {
"error_fetching_user": "Erreur lors de la recherche de l'utilisateur",
"find_user": "Chercher un utilisateur"
},
"general": {
"apply": "Appliquer",
"submit": "Envoyer"
},
"login": {
"login": "Connexion",
"description": "Connexion avec OAuth",
"logout": "Déconnexion",
"password": "Mot de passe",
"placeholder": "p.e. lain",
"register": "S'inscrire",
"username": "Identifiant"
},
"nav": {
"chat": "Chat local",
"friend_requests": "Demandes d'ami",
"dms": "Messages adressés",
"mentions": "Notifications",
"public_tl": "Statuts locaux",
"timeline": "Journal",
"twkn": "Le réseau connu"
},
"notifications": {
"broken_favorite": "Chargement d'un message inconnu ...",
"favorited_you": "a aimé votre statut",
"followed_you": "a commencé à vous suivre",
"load_older": "Charger les notifications précédentes",
"notifications": "Notifications",
"read": "Lu !",
"repeated_you": "a partagé votre statut"
},
"post_status": {
"account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.",
"account_not_locked_warning_link": "verrouillé",
"attachments_sensitive": "Marquer le média comme sensible",
"content_type": {
"text/plain": "Texte brut"
"chat": {
"title": "Chat"
},
"content_warning": "Sujet (optionnel)",
"default": "Écrivez ici votre prochain statut.",
"direct_warning": "Ce message sera visible à toutes les personnes mentionnées.",
"posting": "Envoi en cours",
"scope": {
"direct": "Direct - N'envoyer qu'aux personnes mentionnées",
"private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets",
"public": "Publique - Afficher dans les fils publics",
"unlisted": "Non-Listé - Ne pas afficher dans les fils publics"
"exporter": {
"export": "Exporter",
"processing": "En cours de traitement, vous pourrez bientôt télécharger votre fichier"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Proxy média",
"scope_options": "Options de visibilité",
"text_limit": "Limite de texte",
"title": "Caractéristiques",
"who_to_follow": "Personnes à suivre"
},
"finder": {
"error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice",
"find_user": "Chercher un-e utilisateur·ice"
},
"general": {
"apply": "Appliquer",
"submit": "Envoyer",
"more": "Plus",
"generic_error": "Une erreur s'est produite",
"optional": "optionnel",
"show_more": "Montrer plus",
"show_less": "Montrer moins",
"cancel": "Annuler",
"disable": "Désactiver",
"enable": "Activer",
"confirm": "Confirmer",
"verify": "Vérifier"
},
"image_cropper": {
"crop_picture": "Rogner l'image",
"save": "Sauvegarder",
"save_without_cropping": "Sauvegarder sans rogner",
"cancel": "Annuler"
},
"importer": {
"submit": "Soumettre",
"success": "Importé avec succès.",
"error": "Une erreur est survenue pendant l'import de ce fichier."
},
"login": {
"login": "Connexion",
"description": "Connexion avec OAuth",
"logout": "Déconnexion",
"password": "Mot de passe",
"placeholder": "p.e. lain",
"register": "S'inscrire",
"username": "Identifiant",
"hint": "Connectez-vous pour rejoindre la discussion",
"authentication_code": "Code d'authentification",
"enter_recovery_code": "Entrez un code de récupération",
"enter_two_factor_code": "Entrez un code à double authentification",
"recovery_code": "Code de récupération",
"heading": {
"totp": "Authentification à double authentification",
"recovery": "Récuperation de la double authentification"
}
},
"media_modal": {
"previous": "Précédent",
"next": "Suivant"
},
"nav": {
"about": "À propos",
"back": "Retour",
"chat": "Chat local",
"friend_requests": "Demandes de suivi",
"mentions": "Notifications",
"interactions": "Interactions",
"dms": "Messages directs",
"public_tl": "Fil d'actualité public",
"timeline": "Fil d'actualité",
"twkn": "Ensemble du réseau connu",
"user_search": "Recherche d'utilisateur·ice",
"who_to_follow": "Qui suivre",
"preferences": "Préférences"
},
"notifications": {
"broken_favorite": "Chargement d'un message inconnu…",
"favorited_you": "a aimé votre statut",
"followed_you": "a commencé à vous suivre",
"load_older": "Charger les notifications précédentes",
"notifications": "Notifications",
"read": "Lu !",
"repeated_you": "a partagé votre statut",
"no_more_notifications": "Aucune notification supplémentaire"
},
"interactions": {
"favs_repeats": "Partages et favoris",
"follows": "Nouveaux⋅elles abonné⋅e⋅s ?",
"load_older": "Chargez d'anciennes interactions"
},
"post_status": {
"new_status": "Poster un nouveau statut",
"account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.",
"account_not_locked_warning_link": "verrouillé",
"attachments_sensitive": "Marquer le média comme sensible",
"content_type": {
"text/plain": "Texte brut",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_warning": "Sujet (optionnel)",
"default": "Écrivez ici votre prochain statut.",
"direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.",
"direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.",
"posting": "Envoi en cours",
"scope_notice": {
"public": "Ce statut sera visible par tout le monde",
"private": "Ce statut sera visible par seulement vos abonné⋅e⋅s",
"unlisted": "Ce statut ne sera pas visible dans le Fil d'actualité public et l'Ensemble du réseau connu"
},
"scope": {
"direct": "Direct - N'envoyer qu'aux personnes mentionnées",
"private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets",
"public": "Publique - Afficher dans les fils publics",
"unlisted": "Non-Listé - Ne pas afficher dans les fils publics"
}
},
"registration": {
"bio": "Biographie",
"email": "Adresse mail",
"fullname": "Pseudonyme",
"password_confirm": "Confirmation du mot de passe",
"registration": "Inscription",
"token": "Jeton d'invitation",
"captcha": "CAPTCHA",
"new_captcha": "Cliquez sur l'image pour avoir un nouveau captcha",
"username_placeholder": "p.e. lain",
"fullname_placeholder": "p.e. Lain Iwakura",
"bio_placeholder": "p.e.\nSalut, je suis Lain\nJe suis une héroïne d'animé qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.",
"validations": {
"username_required": "ne peut pas être laissé vide",
"fullname_required": "ne peut pas être laissé vide",
"email_required": "ne peut pas être laissé vide",
"password_required": "ne peut pas être laissé vide",
"password_confirmation_required": "ne peut pas être laissé vide",
"password_confirmation_match": "doit être identique au mot de passe"
}
},
"selectable_list": {
"select_all": "Tout selectionner"
},
"settings": {
"app_name": "Nom de l'application",
"security": "Sécurité",
"enter_current_password_to_confirm": "Entrez votre mot de passe actuel pour confirmer votre identité",
"mfa": {
"otp": "OTP",
"setup_otp": "Configurer OTP",
"wait_pre_setup_otp": "préconfiguration OTP",
"confirm_and_enable": "Confirmer & activer OTP",
"title": "Double authentification",
"generate_new_recovery_codes": "Générer de nouveaux codes de récupération",
"warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.",
"recovery_codes": "Codes de récupération.",
"waiting_a_recovery_codes": "Récéption des codes de récupération…",
"recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.",
"authentication_methods": "Methodes d'authentification",
"scan": {
"title": "Scanner",
"desc": "En utilisant votre application de double authentification, scannez ce QR code ou entrez la clé textuelle :",
"secret_code": "Clé"
},
"verify": {
"desc": "Pour activer la double authentification, entrez le code depuis votre application:"
}
},
"attachmentRadius": "Pièces jointes",
"attachments": "Pièces jointes",
"autoload": "Charger la suite automatiquement une fois le bas de la page atteint",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarRadius": "Avatars",
"background": "Arrière-plan",
"bio": "Biographie",
"block_export": "Export des comptes bloqués",
"block_export_button": "Export des comptes bloqués vers un fichier csv",
"block_import": "Import des comptes bloqués",
"block_import_error": "Erreur lors de l'import des comptes bloqués",
"blocks_imported": "Blocks importés! Le traitement va prendre un moment.",
"blocks_tab": "Bloqué·e·s",
"btnRadius": "Boutons",
"cBlue": "Bleu (répondre, suivre)",
"cGreen": "Vert (partager)",
"cOrange": "Orange (aimer)",
"cRed": "Rouge (annuler)",
"change_password": "Changez votre mot de passe",
"change_password_error": "Il y a eu un problème pour changer votre mot de passe.",
"changed_password": "Mot de passe modifié avec succès !",
"collapse_subject": "Réduire les messages avec des sujets",
"composing": "Composition",
"confirm_new_password": "Confirmation du nouveau mot de passe",
"current_avatar": "Avatar actuel",
"current_password": "Mot de passe actuel",
"current_profile_banner": "Bannière de profil actuelle",
"data_import_export_tab": "Import / Export des Données",
"default_vis": "Visibilité par défaut",
"delete_account": "Supprimer le compte",
"delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.",
"delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.",
"delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.",
"avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.",
"export_theme": "Enregistrer le thème",
"filtering": "Filtre",
"filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne",
"follow_export": "Exporter les abonnements",
"follow_export_button": "Exporter les abonnements en csv",
"follow_import": "Importer des abonnements",
"follow_import_error": "Erreur lors de l'importation des abonnements",
"follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.",
"foreground": "Premier plan",
"general": "Général",
"hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations",
"hide_attachments_in_tl": "Masquer les pièces jointes dans le journal",
"hide_muted_posts": "Masquer les statuts des utilisateurs masqués",
"max_thumbnails": "Nombre maximum de miniatures par statuts",
"hide_isp": "Masquer le panneau spécifique a l'instance",
"preload_images": "Précharger les images",
"use_one_click_nsfw": "Ouvrir les pièces-jointes NSFW avec un seul clic",
"hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)",
"hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)",
"hide_filtered_statuses": "Masquer les statuts filtrés",
"import_blocks_from_a_csv_file": "Importer les blocages depuis un fichier csv",
"import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv",
"import_theme": "Charger le thème",
"inputRadius": "Champs de texte",
"checkboxRadius": "Cases à cocher",
"instance_default": "(default: {value})",
"instance_default_simple": "(default)",
"interface": "Interface",
"interfaceLanguage": "Langue de l'interface",
"invalid_theme_imported": "Le fichier sélectionné n'est pas un thème Pleroma pris en charge. Aucun changement n'a été apporté à votre thème.",
"limited_availability": "Non disponible dans votre navigateur",
"links": "Liens",
"lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement",
"loop_video": "Vidéos en boucle",
"loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les « gifs » de Mastodon)",
"mutes_tab": "Comptes silenciés",
"play_videos_in_modal": "Jouer les vidéos directement dans le visionneur de médias",
"use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes",
"name": "Nom",
"name_bio": "Nom & Bio",
"new_password": "Nouveau mot de passe",
"notification_visibility": "Types de notifications à afficher",
"notification_visibility_follows": "Abonnements",
"notification_visibility_likes": "J'aime",
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"no_rich_text_description": "Ne formatez pas le texte",
"no_blocks": "Aucun bloqués",
"no_mutes": "Aucun masqués",
"hide_follows_description": "Ne pas afficher à qui je suis abonné",
"hide_followers_description": "Ne pas afficher qui est abonné à moi",
"show_admin_badge": "Afficher le badge d'Administrateur⋅ice sur mon profil",
"show_moderator_badge": "Afficher le badge de Modérateur⋅ice sur mon profil",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas actif",
"presets": "Thèmes prédéfinis",
"profile_background": "Image de fond",
"profile_banner": "Bannière de profil",
"profile_tab": "Profil",
"radii_help": "Vous pouvez ici choisir le niveau d'arrondi des angles de l'interface (en pixels)",
"replies_in_timeline": "Réponses au journal",
"reply_link_preview": "Afficher un aperçu lors du survol de liens vers une réponse",
"reply_visibility_all": "Montrer toutes les réponses",
"reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis",
"reply_visibility_self": "Afficher uniquement les réponses adressées à moi",
"autohide_floating_post_button": "Automatiquement cacher le bouton de Nouveau Statut (sur mobile)",
"saving_err": "Erreur lors de l'enregistrement des paramètres",
"saving_ok": "Paramètres enregistrés",
"search_user_to_block": "Rechercher qui vous voulez bloquer",
"search_user_to_mute": "Rechercher qui vous voulez masquer",
"security_tab": "Sécurité",
"scope_copy": "Garder la même visibilité en répondant (les DMs restent toujours des DMs)",
"minimal_scopes_mode": "Rétrécir les options de séléction de la portée",
"set_new_avatar": "Changer d'avatar",
"set_new_profile_background": "Changer d'image de fond",
"set_new_profile_banner": "Changer de bannière",
"settings": "Paramètres",
"subject_input_always_show": "Toujours copier le champ de sujet",
"subject_line_behavior": "Copier le sujet en répondant",
"subject_line_email": "Comme les mails: « re: sujet »",
"subject_line_mastodon": "Comme mastodon: copier tel quel",
"subject_line_noop": "Ne pas copier",
"post_status_content_type": "Type de contenu du statuts",
"stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris",
"streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page",
"text": "Texte",
"theme": "Thème",
"theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.",
"theme_help_v2_1": "Vous pouvez aussi surcharger certaines couleurs de composants et transparence via la case à cocher, utilisez le bouton « Vider tout » pour effacer toutes les surcharges.",
"theme_help_v2_2": "Les icônes sous certaines des entrées ont un indicateur de contraste du fond/texte, survolez les pour plus d'informations détailles. Veuillez garder a l'esprit que lors de l'utilisation de transparence l'indicateur de contraste indique le pire des cas.",
"tooltipRadius": "Info-bulles/alertes",
"upload_a_photo": "Envoyer une photo",
"user_settings": "Paramètres utilisateur",
"values": {
"false": "non",
"true": "oui"
},
"notifications": "Notifications",
"notification_setting": "Reçevoir les notifications de:",
"notification_setting_follows": "Utilisateurs que vous suivez",
"notification_setting_non_follows": "Utilisateurs que vous ne suivez pas",
"notification_setting_followers": "Utilisateurs qui vous suivent",
"notification_setting_non_followers": "Utilisateurs qui ne vous suivent pas",
"notification_mutes": "Pour stopper la récéption de notifications d'un utilisateur particulier, utilisez un masquage.",
"notification_blocks": "Bloquer un utilisateur stoppe toute notification et se désabonne de lui.",
"enable_web_push_notifications": "Activer les notifications de push web",
"style": {
"switcher": {
"keep_color": "Garder les couleurs",
"keep_shadows": "Garder les ombres",
"keep_opacity": "Garder la transparence",
"keep_roundness": "Garder la rondeur",
"keep_fonts": "Garder les polices",
"save_load_hint": "L'option « Garder » préserve les options activés en cours lors de la séléction ou chargement des thèmes, il sauve aussi les dites options lors de l'export d'un thème. Quand toutes les cases sont décochés, exporter un thème sauvera tout.",
"reset": "Remise à zéro",
"clear_all": "Tout vider",
"clear_opacity": "Vider la transparence"
},
"common": {
"color": "Couleur",
"opacity": "Transparence",
"contrast": {
"hint": "Le ratio de contraste est {ratio}, il {level} {context}",
"level": {
"aa": "répond aux directives de niveau AA (minimum)",
"aaa": "répond aux directives de niveau AAA (recommandé)",
"bad": "ne réponds à aucune directive d'accessibilité"
},
"context": {
"18pt": "pour texte large (19pt+)",
"text": "pour texte"
}
}
},
"common_colors": {
"_tab_label": "Commun",
"main": "Couleurs communes",
"foreground_hint": "Voir l'onglet « Avancé » pour plus de contrôle détaillé",
"rgbo": "Icônes, accents, badges"
},
"advanced_colors": {
"_tab_label": "Avancé",
"alert": "Fond d'alerte",
"alert_error": "Erreur",
"badge": "Fond de badge",
"badge_notification": "Notification",
"panel_header": "Entête de panneau",
"top_bar": "Barre du haut",
"borders": "Bordures",
"buttons": "Boutons",
"inputs": "Champs de saisie",
"faint_text": "Texte en fondu"
},
"radii": {
"_tab_label": "Rondeur"
},
"shadows": {
"_tab_label": "Ombres et éclairage",
"component": "Composant",
"override": "Surcharger",
"shadow_id": "Ombre #{value}",
"blur": "Flou",
"spread": "Dispersion",
"inset": "Interne",
"hint": "Pour les ombres, vous pouvez aussi utiliser --variable comme valeur de couleur en CSS3. Veuillez noter que spécifier la transparence ne fonctionnera pas dans ce cas.",
"filter_hint": {
"always_drop_shadow": "Attention, cette ombre utilise toujours {0} quand le navigateur le supporte.",
"drop_shadow_syntax": "{0} ne supporte pas le paramètre {1} et mot-clé {2}.",
"avatar_inset": "Veuillez noter que combiner a la fois les ombres internes et non-internes sur les avatars peut fournir des résultats innatendus avec la transparence des avatars.",
"spread_zero": "Les ombres avec une dispersion > 0 apparaitrons comme si ils étaient à zéro",
"inset_classic": "L'ombre interne utilisera toujours {0}"
},
"components": {
"panel": "Panneau",
"panelHeader": "En-tête de panneau",
"topBar": "Barre du haut",
"avatar": "Avatar utilisateur⋅ice (dans la vue de profil)",
"avatarStatus": "Avatar utilisateur⋅ice (dans la vue de statuts)",
"popup": "Popups et infobulles",
"button": "Bouton",
"buttonHover": "Bouton (survol)",
"buttonPressed": "Bouton (cliqué)",
"buttonPressedHover": "Bouton (cliqué+survol)",
"input": "Champ de saisie"
}
},
"fonts": {
"_tab_label": "Polices",
"help": "Sélectionnez la police à utiliser pour les éléments de l'UI. Pour « personnalisé » vous avez à entrer le nom exact de la police comme il apparaît dans le système.",
"components": {
"interface": "Interface",
"input": "Champs de saisie",
"post": "Post text",
"postCode": "Texte à taille fixe dans un article (texte enrichi)"
},
"family": "Nom de la police",
"size": "Taille (en px)",
"weight": "Poid (gras)",
"custom": "Personnalisé"
},
"preview": {
"header": "Prévisualisation",
"content": "Contenu",
"error": "Exemple d'erreur",
"button": "Bouton",
"text": "Un certain nombre de {0} et {1}",
"mono": "contenu",
"input": "Je viens juste datterrir à L.A.",
"faint_link": "manuel utile",
"fine_print": "Lisez notre {0} pour n'apprendre rien d'utile !",
"header_faint": "Tout va bien",
"checkbox": "J'ai survolé les conditions d'utilisation",
"link": "un petit lien sympa"
}
},
"version": {
"title": "Version",
"backend_version": "Version du Backend",
"frontend_version": "Version du Frontend"
}
},
"timeline": {
"collapse": "Fermer",
"conversation": "Conversation",
"error_fetching": "Erreur en cherchant les mises à jour",
"load_older": "Afficher plus",
"no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être partagé",
"repeated": "a partagé",
"show_new": "Afficher plus",
"up_to_date": "À jour",
"no_more_statuses": "Pas plus de statuts",
"no_statuses": "Aucun statuts"
},
"status": {
"favorites": "Favoris",
"repeats": "Partages",
"delete": "Supprimer statuts",
"pin": "Agraffer sur le profil",
"unpin": "Dégraffer du profil",
"pinned": "Agraffé",
"delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?",
"reply_to": "Réponse à",
"replies_list": "Réponses:"
},
"user_card": {
"approve": "Accepter",
"block": "Bloquer",
"blocked": "Bloqué !",
"deny": "Rejeter",
"favorites": "Favoris",
"follow": "Suivre",
"follow_sent": "Demande envoyée !",
"follow_progress": "Demande en cours…",
"follow_again": "Renvoyer la demande ?",
"follow_unfollow": "Désabonner",
"followees": "Suivis",
"followers": "Vous suivent",
"following": "Suivi !",
"follows_you": "Vous suit !",
"its_you": "C'est vous !",
"media": "Media",
"mute": "Masquer",
"muted": "Masqué",
"per_day": "par jour",
"remote_follow": "Suivre d'une autre instance",
"report": "Signalement",
"statuses": "Statuts",
"unblock": "Débloquer",
"unblock_progress": "Déblocage…",
"block_progress": "Blocage…",
"unmute": "Démasquer",
"unmute_progress": "Démasquage…",
"mute_progress": "Masquage…",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Promouvoir Administrateur⋅ice",
"revoke_admin": "Dégrader Administrateur⋅ice",
"grant_moderator": "Promouvoir Modérateur⋅ice",
"revoke_moderator": "Dégrader Modérateur⋅ice",
"activate_account": "Activer le compte",
"deactivate_account": "Désactiver le compte",
"delete_account": "Supprimer le compte",
"force_nsfw": "Marquer tous les statuts comme NSFW",
"strip_media": "Supprimer les medias des statuts",
"force_unlisted": "Forcer les statuts à être délistés",
"sandbox": "Forcer les statuts à être visibles seuleument pour les abonné⋅e⋅s",
"disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante",
"disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court",
"quarantine": "Interdir les statuts de l'utilisateur à fédérer",
"delete_user": "Supprimer l'utilisateur",
"delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée."
}
},
"user_profile": {
"timeline_title": "Journal de l'utilisateur⋅ice",
"profile_does_not_exist": "Désolé, ce profil n'existe pas.",
"profile_loading_error": "Désolé, il y a eu une erreur au chargement du profil."
},
"user_reporting": {
"title": "Signaler {0}",
"add_comment_description": "Ce signalement sera envoyé aux modérateur⋅ice⋅s de votre instance. Vous pouvez fournir une explication de pourquoi vous signalez ce compte ci-dessous :",
"additional_comments": "Commentaires additionnels",
"forward_description": "Le compte vient d'un autre serveur. Envoyer une copie du signalement à celui-ci aussi ?",
"forward_to": "Transmettre à {0}",
"submit": "Envoyer",
"generic_error": "Une erreur est survenue lors du traitement de votre requête."
},
"who_to_follow": {
"more": "Plus",
"who_to_follow": "À qui s'abonner"
},
"tool_tip": {
"media_upload": "Envoyer un media",
"repeat": "Répéter",
"reply": "Répondre",
"favorite": "Favoriser",
"user_settings": "Paramètres utilisateur"
},
"upload": {
"error": {
"base": "L'envoi a échoué.",
"file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Réessayez plus tard"
},
"file_size_units": {
"B": "O",
"KiB": "KiO",
"MiB": "MiO",
"GiB": "GiO",
"TiB": "TiO"
}
}
},
"registration": {
"bio": "Biographie",
"email": "Adresse email",
"fullname": "Pseudonyme",
"password_confirm": "Confirmation du mot de passe",
"registration": "Inscription",
"token": "Jeton d'invitation"
},
"settings": {
"attachmentRadius": "Pièces jointes",
"attachments": "Pièces jointes",
"autoload": "Charger la suite automatiquement une fois le bas de la page atteint",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarRadius": "Avatars",
"background": "Arrière-plan",
"bio": "Biographie",
"btnRadius": "Boutons",
"cBlue": "Bleu (Répondre, suivre)",
"cGreen": "Vert (Partager)",
"cOrange": "Orange (Aimer)",
"cRed": "Rouge (Annuler)",
"change_password": "Changez votre mot de passe",
"change_password_error": "Il y a eu un problème pour changer votre mot de passe.",
"changed_password": "Mot de passe modifié avec succès !",
"collapse_subject": "Réduire les messages avec des sujets",
"confirm_new_password": "Confirmation du nouveau mot de passe",
"current_avatar": "Avatar actuel",
"current_password": "Mot de passe actuel",
"current_profile_banner": "Bannière de profil actuelle",
"data_import_export_tab": "Import / Export des Données",
"default_vis": "Portée de visibilité par défaut",
"delete_account": "Supprimer le compte",
"delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.",
"delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur de cette instance.",
"delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.",
"export_theme": "Enregistrer le thème",
"filtering": "Filtre",
"filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne",
"follow_export": "Exporter les abonnements",
"follow_export_button": "Exporter les abonnements en csv",
"follow_export_processing": "Exportation en cours…",
"follow_import": "Importer des abonnements",
"follow_import_error": "Erreur lors de l'importation des abonnements",
"follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.",
"foreground": "Premier plan",
"general": "Général",
"hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations",
"hide_attachments_in_tl": "Masquer les pièces jointes dans le journal",
"hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)",
"hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)",
"import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv",
"import_theme": "Charger le thème",
"inputRadius": "Champs de texte",
"instance_default": "(default: {value})",
"instance_default_simple" : "(default)",
"interfaceLanguage": "Langue de l'interface",
"invalid_theme_imported": "Le fichier sélectionné n'est pas un thème Pleroma pris en charge. Aucun changement n'a été apporté à votre thème.",
"limited_availability": "Non disponible dans votre navigateur",
"links": "Liens",
"lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement",
"loop_video": "Vidéos en boucle",
"loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les «gifs» de Mastodon)",
"name": "Nom",
"name_bio": "Nom & Bio",
"new_password": "Nouveau mot de passe",
"no_rich_text_description": "Ne formatez pas le texte",
"notification_visibility": "Types de notifications à afficher",
"notification_visibility_follows": "Abonnements",
"notification_visibility_likes": "Jaime",
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis",
"profile_background": "Image de fond",
"profile_banner": "Bannière de profil",
"profile_tab": "Profil",
"radii_help": "Vous pouvez ici choisir le niveau d'arrondi des angles de l'interface (en pixels)",
"replies_in_timeline": "Réponses au journal",
"reply_link_preview": "Afficher un aperçu lors du survol de liens vers une réponse",
"reply_visibility_all": "Montrer toutes les réponses",
"reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux utilisateurs que je suis",
"reply_visibility_self": "Afficher uniquement les réponses adressées à moi",
"saving_err": "Erreur lors de l'enregistrement des paramètres",
"saving_ok": "Paramètres enregistrés",
"security_tab": "Sécurité",
"set_new_avatar": "Changer d'avatar",
"set_new_profile_background": "Changer d'image de fond",
"set_new_profile_banner": "Changer de bannière",
"settings": "Paramètres",
"stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris",
"streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page",
"text": "Texte",
"theme": "Thème",
"theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.",
"tooltipRadius": "Info-bulles/alertes",
"user_settings": "Paramètres utilisateur",
"values": {
"false": "non",
"true": "oui"
}
},
"timeline": {
"collapse": "Fermer",
"conversation": "Conversation",
"error_fetching": "Erreur en cherchant les mises à jour",
"load_older": "Afficher plus",
"no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être répété",
"repeated": "a partagé",
"show_new": "Afficher plus",
"up_to_date": "À jour"
},
"user_card": {
"approve": "Accepter",
"block": "Bloquer",
"blocked": "Bloqué !",
"deny": "Rejeter",
"follow": "Suivre",
"followees": "Suivis",
"followers": "Vous suivent",
"following": "Suivi !",
"follows_you": "Vous suit !",
"mute": "Masquer",
"muted": "Masqué",
"per_day": "par jour",
"remote_follow": "Suivre d'une autre instance",
"statuses": "Statuts"
},
"user_profile": {
"timeline_title": "Journal de l'utilisateur"
},
"who_to_follow": {
"more": "Plus",
"who_to_follow": "Qui s'abonner"
}
}

View file

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

View file

@ -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": "スレッド",

View file

@ -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": "スレッド",

View file

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

View file

@ -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)

View file

@ -59,7 +59,7 @@ const api = {
// Set up websocket connection
if (!store.state.chatDisabled) {
const token = store.state.wsToken
const socket = new Socket('/socket', {params: {token}})
const socket = new Socket('/socket', { params: { token } })
socket.connect()
store.dispatch('initializeChat', socket)
}

View file

@ -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 = {

View file

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

View file

@ -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) => {
@ -104,7 +106,7 @@ const promisedRequest = ({ method, url, payload, credentials, headers = {} }) =>
})
}
const updateNotificationSettings = ({credentials, settings}) => {
const updateNotificationSettings = ({ credentials, settings }) => {
const form = new FormData()
each(settings, (value, key) => {
@ -115,20 +117,18 @@ const updateNotificationSettings = ({credentials, settings}) => {
headers: authHeaders(credentials),
method: 'PUT',
body: form
})
.then((data) => data.json())
}).then((data) => data.json())
}
const updateAvatar = ({credentials, avatar}) => {
const updateAvatar = ({ credentials, avatar }) => {
const form = new FormData()
form.append('avatar', avatar)
return fetch(MASTODON_PROFILE_UPDATE_URL, {
headers: authHeaders(credentials),
method: 'PATCH',
body: form
})
.then((data) => data.json())
.then((data) => parseUser(data))
}).then((data) => data.json())
.then((data) => parseUser(data))
}
const updateBg = ({ credentials, background }) => {
@ -143,26 +143,24 @@ const updateBg = ({ credentials, background }) => {
.then((data) => parseUser(data))
}
const updateBanner = ({credentials, banner}) => {
const updateBanner = ({ credentials, banner }) => {
const form = new FormData()
form.append('header', banner)
return fetch(MASTODON_PROFILE_UPDATE_URL, {
headers: authHeaders(credentials),
method: 'PATCH',
body: form
})
.then((data) => data.json())
.then((data) => parseUser(data))
}).then((data) => data.json())
.then((data) => parseUser(data))
}
const updateProfile = ({credentials, params}) => {
const updateProfile = ({ credentials, params }) => {
return promisedRequest({
url: MASTODON_PROFILE_UPDATE_URL,
method: 'PATCH',
payload: params,
credentials
})
.then((data) => parseUser(data))
}).then((data) => parseUser(data))
}
// Params needed:
@ -212,7 +210,7 @@ const authHeaders = (accessToken) => {
}
}
const externalProfile = ({profileUrl, credentials}) => {
const externalProfile = ({ profileUrl, credentials }) => {
let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}`
return fetch(url, {
headers: authHeaders(credentials),
@ -220,7 +218,7 @@ const externalProfile = ({profileUrl, credentials}) => {
}).then((data) => data.json())
}
const followUser = ({id, credentials}) => {
const followUser = ({ id, credentials }) => {
let url = MASTODON_FOLLOW_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
@ -228,7 +226,7 @@ const followUser = ({id, credentials}) => {
}).then((data) => data.json())
}
const unfollowUser = ({id, credentials}) => {
const unfollowUser = ({ id, credentials }) => {
let url = MASTODON_UNFOLLOW_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
@ -246,21 +244,21 @@ const unpinOwnStatus = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
const blockUser = ({id, credentials}) => {
const blockUser = ({ id, credentials }) => {
return fetch(MASTODON_BLOCK_USER_URL(id), {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const unblockUser = ({id, credentials}) => {
const unblockUser = ({ id, credentials }) => {
return fetch(MASTODON_UNBLOCK_USER_URL(id), {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const approveUser = ({id, credentials}) => {
const approveUser = ({ id, credentials }) => {
let url = `${APPROVE_USER_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
@ -268,7 +266,7 @@ const approveUser = ({id, credentials}) => {
}).then((data) => data.json())
}
const denyUser = ({id, credentials}) => {
const denyUser = ({ id, credentials }) => {
let url = `${DENY_USER_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
@ -276,13 +274,13 @@ const denyUser = ({id, credentials}) => {
}).then((data) => data.json())
}
const fetchUser = ({id, credentials}) => {
const fetchUser = ({ id, credentials }) => {
let url = `${MASTODON_USER_URL}/${id}`
return promisedRequest({ url, credentials })
.then((data) => parseUser(data))
}
const fetchUserRelationship = ({id, credentials}) => {
const fetchUserRelationship = ({ id, credentials }) => {
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
@ -296,7 +294,7 @@ const fetchUserRelationship = ({id, credentials}) => {
})
}
const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
let url = MASTODON_FOLLOWING_URL(id)
const args = [
maxId && `max_id=${maxId}`,
@ -310,7 +308,7 @@ const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
.then((data) => data.map(parseUser))
}
const exportFriends = ({id, credentials}) => {
const exportFriends = ({ id, credentials }) => {
return new Promise(async (resolve, reject) => {
try {
let friends = []
@ -330,7 +328,7 @@ const exportFriends = ({id, credentials}) => {
})
}
const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
let url = MASTODON_FOLLOWERS_URL(id)
const args = [
maxId && `max_id=${maxId}`,
@ -344,13 +342,13 @@ const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
.then((data) => data.map(parseUser))
}
const fetchFollowRequests = ({credentials}) => {
const fetchFollowRequests = ({ credentials }) => {
const url = FOLLOW_REQUESTS_URL
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
}
const fetchConversation = ({id, credentials}) => {
const fetchConversation = ({ id, credentials }) => {
let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
return fetch(urlContext, { headers: authHeaders(credentials) })
.then((data) => {
@ -360,13 +358,13 @@ const fetchConversation = ({id, credentials}) => {
throw new Error('Error fetching timeline', data)
})
.then((data) => data.json())
.then(({ancestors, descendants}) => ({
.then(({ ancestors, descendants }) => ({
ancestors: ancestors.map(parseStatus),
descendants: descendants.map(parseStatus)
}))
}
const fetchStatus = ({id, credentials}) => {
const fetchStatus = ({ id, credentials }) => {
let url = MASTODON_STATUS_URL(id)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
@ -379,7 +377,7 @@ const fetchStatus = ({id, credentials}) => {
.then((data) => parseStatus(data))
}
const tagUser = ({tag, credentials, ...options}) => {
const tagUser = ({ tag, credentials, ...options }) => {
const screenName = options.screen_name
const form = {
nicknames: [screenName],
@ -396,7 +394,7 @@ const tagUser = ({tag, credentials, ...options}) => {
})
}
const untagUser = ({tag, credentials, ...options}) => {
const untagUser = ({ tag, credentials, ...options }) => {
const screenName = options.screen_name
const body = {
nicknames: [screenName],
@ -413,7 +411,7 @@ const untagUser = ({tag, credentials, ...options}) => {
})
}
const addRight = ({right, credentials, ...user}) => {
const addRight = ({ right, credentials, ...user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@ -423,7 +421,7 @@ const addRight = ({right, credentials, ...user}) => {
})
}
const deleteRight = ({right, credentials, ...user}) => {
const deleteRight = ({ right, credentials, ...user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@ -433,7 +431,7 @@ const deleteRight = ({right, credentials, ...user}) => {
})
}
const setActivationStatus = ({status, credentials, ...user}) => {
const setActivationStatus = ({ status, credentials, ...user }) => {
const screenName = user.screen_name
const body = {
status: status
@ -449,7 +447,7 @@ const setActivationStatus = ({status, credentials, ...user}) => {
})
}
const deleteUser = ({credentials, ...user}) => {
const deleteUser = ({ credentials, ...user }) => {
const screenName = user.screen_name
const headers = authHeaders(credentials)
@ -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)
}
@ -598,7 +628,7 @@ const deleteStatus = ({ id, credentials }) => {
})
}
const uploadMedia = ({formData, credentials}) => {
const uploadMedia = ({ formData, credentials }) => {
return fetch(MASTODON_MEDIA_UPLOAD_URL, {
body: formData,
method: 'POST',
@ -608,7 +638,7 @@ const uploadMedia = ({formData, credentials}) => {
.then((data) => parseAttachment(data))
}
const importBlocks = ({file, credentials}) => {
const importBlocks = ({ file, credentials }) => {
const formData = new FormData()
formData.append('list', file)
return fetch(BLOCKS_IMPORT_URL, {
@ -619,7 +649,7 @@ const importBlocks = ({file, credentials}) => {
.then((response) => response.ok)
}
const importFollows = ({file, credentials}) => {
const importFollows = ({ file, credentials }) => {
const formData = new FormData()
formData.append('list', file)
return fetch(FOLLOW_IMPORT_URL, {
@ -630,7 +660,7 @@ const importFollows = ({file, credentials}) => {
.then((response) => response.ok)
}
const deleteAccount = ({credentials, password}) => {
const deleteAccount = ({ credentials, password }) => {
const form = new FormData()
form.append('password', password)
@ -643,7 +673,7 @@ const deleteAccount = ({credentials, password}) => {
.then((response) => response.json())
}
const changePassword = ({credentials, password, newPassword, newPasswordConfirmation}) => {
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData()
form.append('password', password)
@ -658,14 +688,14 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
.then((response) => response.json())
}
const settingsMFA = ({credentials}) => {
const settingsMFA = ({ credentials }) => {
return fetch(MFA_SETTINGS_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const mfaDisableOTP = ({credentials, password}) => {
const mfaDisableOTP = ({ credentials, password }) => {
const form = new FormData()
form.append('password', password)
@ -678,7 +708,7 @@ const mfaDisableOTP = ({credentials, password}) => {
.then((response) => response.json())
}
const mfaConfirmOTP = ({credentials, password, token}) => {
const mfaConfirmOTP = ({ credentials, password, token }) => {
const form = new FormData()
form.append('password', password)
@ -690,38 +720,38 @@ const mfaConfirmOTP = ({credentials, password, token}) => {
method: 'POST'
}).then((data) => data.json())
}
const mfaSetupOTP = ({credentials}) => {
const mfaSetupOTP = ({ credentials }) => {
return fetch(MFA_SETUP_OTP_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const generateMfaBackupCodes = ({credentials}) => {
const generateMfaBackupCodes = ({ credentials }) => {
return fetch(MFA_BACKUP_CODES_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const fetchMutes = ({credentials}) => {
const fetchMutes = ({ credentials }) => {
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
.then((users) => users.map(parseUser))
}
const muteUser = ({id, credentials}) => {
const muteUser = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' })
}
const unmuteUser = ({id, credentials}) => {
const unmuteUser = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
}
const fetchBlocks = ({credentials}) => {
const fetchBlocks = ({ credentials }) => {
return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
.then((users) => users.map(parseUser))
}
const fetchOAuthTokens = ({credentials}) => {
const fetchOAuthTokens = ({ credentials }) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
@ -734,7 +764,7 @@ const fetchOAuthTokens = ({credentials}) => {
})
}
const revokeOAuthToken = ({id, credentials}) => {
const revokeOAuthToken = ({ id, credentials }) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
@ -743,13 +773,13 @@ const revokeOAuthToken = ({id, credentials}) => {
})
}
const suggestions = ({credentials}) => {
const suggestions = ({ credentials }) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const markNotificationsAsSeen = ({id, credentials}) => {
const markNotificationsAsSeen = ({ id, credentials }) => {
const body = new FormData()
body.append('latest_id', id)
@ -761,15 +791,39 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).then((data) => data.json())
}
const fetchFavoritedByUsers = ({id}) => {
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))
}
const fetchRebloggedByUsers = ({id}) => {
const fetchRebloggedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
}
const reportUser = ({credentials, userId, statusIds, comment, forward}) => {
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
return promisedRequest({
url: MASTODON_REPORT_USER_URL,
method: 'POST',
@ -840,6 +894,8 @@ const apiService = {
denyUser,
suggestions,
markNotificationsAsSeen,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,

View file

@ -2,57 +2,57 @@ 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 fetchStatus = ({id}) => {
return apiService.fetchStatus({id, credentials})
const backendInteractorService = credentials => {
const fetchStatus = ({ id }) => {
return apiService.fetchStatus({ id, credentials })
}
const fetchConversation = ({id}) => {
return apiService.fetchConversation({id, credentials})
const fetchConversation = ({ id }) => {
return apiService.fetchConversation({ id, credentials })
}
const fetchFriends = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFriends({id, maxId, sinceId, limit, credentials})
const fetchFriends = ({ id, maxId, sinceId, limit }) => {
return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials })
}
const exportFriends = ({id}) => {
return apiService.exportFriends({id, credentials})
const exportFriends = ({ id }) => {
return apiService.exportFriends({ id, credentials })
}
const fetchFollowers = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials})
const fetchFollowers = ({ id, maxId, sinceId, limit }) => {
return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials })
}
const fetchUser = ({id}) => {
return apiService.fetchUser({id, credentials})
const fetchUser = ({ id }) => {
return apiService.fetchUser({ id, credentials })
}
const fetchUserRelationship = ({id}) => {
return apiService.fetchUserRelationship({id, credentials})
const fetchUserRelationship = ({ id }) => {
return apiService.fetchUserRelationship({ id, credentials })
}
const followUser = (id) => {
return apiService.followUser({credentials, id})
return apiService.followUser({ credentials, id })
}
const unfollowUser = (id) => {
return apiService.unfollowUser({credentials, id})
return apiService.unfollowUser({ credentials, id })
}
const blockUser = (id) => {
return apiService.blockUser({credentials, id})
return apiService.blockUser({ credentials, id })
}
const unblockUser = (id) => {
return apiService.unblockUser({credentials, id})
return apiService.unblockUser({ credentials, id })
}
const approveUser = (id) => {
return apiService.approveUser({credentials, id})
return apiService.approveUser({ credentials, id })
}
const denyUser = (id) => {
return apiService.denyUser({credentials, id})
return apiService.denyUser({ credentials, id })
}
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
@ -63,73 +63,83 @@ const backendInteractorService = (credentials) => {
return notificationsFetcher.startFetching({ store, credentials })
}
const tagUser = ({screen_name}, tag) => {
return apiService.tagUser({screen_name, tag, credentials})
const tagUser = ({ screen_name }, tag) => {
return apiService.tagUser({ screen_name, tag, credentials })
}
const untagUser = ({screen_name}, tag) => {
return apiService.untagUser({screen_name, tag, credentials})
const untagUser = ({ screen_name }, tag) => {
return apiService.untagUser({ screen_name, tag, credentials })
}
const addRight = ({screen_name}, right) => {
return apiService.addRight({screen_name, right, credentials})
const addRight = ({ screen_name }, right) => {
return apiService.addRight({ screen_name, right, credentials })
}
const deleteRight = ({screen_name}, right) => {
return apiService.deleteRight({screen_name, right, credentials})
const deleteRight = ({ screen_name }, right) => {
return apiService.deleteRight({ screen_name, right, credentials })
}
const setActivationStatus = ({screen_name}, status) => {
return apiService.setActivationStatus({screen_name, status, credentials})
const setActivationStatus = ({ screen_name }, status) => {
return apiService.setActivationStatus({ screen_name, status, credentials })
}
const deleteUser = ({screen_name}) => {
return apiService.deleteUser({screen_name, credentials})
const deleteUser = ({ screen_name }) => {
return apiService.deleteUser({ screen_name, credentials })
}
const updateNotificationSettings = ({settings}) => {
return apiService.updateNotificationSettings({credentials, settings})
const vote = (pollId, choices) => {
return apiService.vote({ credentials, pollId, choices })
}
const fetchMutes = () => apiService.fetchMutes({credentials})
const muteUser = (id) => apiService.muteUser({credentials, id})
const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
const fetchBlocks = () => apiService.fetchBlocks({credentials})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({credentials, id})
const pinOwnStatus = (id) => apiService.pinOwnStatus({credentials, id})
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id})
const fetchPoll = (pollId) => {
return apiService.fetchPoll({ credentials, pollId })
}
const updateNotificationSettings = ({ settings }) => {
return apiService.updateNotificationSettings({ credentials, settings })
}
const fetchMutes = () => apiService.fetchMutes({ credentials })
const muteUser = (id) => apiService.muteUser({ credentials, id })
const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
const fetchBlocks = () => apiService.fetchBlocks({ credentials })
const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials })
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials })
const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register({ credentials, params })
const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar })
const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
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 externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials })
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
const importFollows = (file) => apiService.importFollows({ file, credentials })
const fetchSettingsMFA = () => apiService.settingsMFA({credentials})
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({credentials})
const mfaSetupOTP = () => apiService.mfaSetupOTP({credentials})
const mfaConfirmOTP = ({password, token}) => apiService.mfaConfirmOTP({credentials, password, token})
const mfaDisableOTP = ({password}) => apiService.mfaDisableOTP({credentials, password})
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
const reportUser = (params) => apiService.reportUser({credentials, ...params})
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
const favorite = (id) => apiService.favorite({id, credentials})
const unfavorite = (id) => apiService.unfavorite({id, credentials})
const retweet = (id) => apiService.retweet({id, credentials})
const unretweet = (id) => apiService.unretweet({id, credentials})
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
const reportUser = (params) => apiService.reportUser({ credentials, ...params })
const favorite = (id) => apiService.favorite({ id, credentials })
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
const retweet = (id) => apiService.retweet({ id, credentials })
const unretweet = (id) => apiService.unretweet({ id, credentials })
const backendInteractorServiceInstance = {
fetchStatus,
@ -180,6 +190,8 @@ const backendInteractorService = (credentials) => {
fetchFollowRequests,
approveUser,
denyUser,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,

View 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
}

View file

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

View file

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

View file

@ -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
View file

0
static/font/README.txt Executable file → Normal file
View file

8
static/font/config.json Executable file → Normal file
View file

@ -240,6 +240,12 @@
"code": 59416,
"src": "fontawesome"
},
{
"uid": "266d5d9adf15a61800477a5acf9a4462",
"css": "chart-bar",
"code": 59419,
"src": "fontawesome"
},
{
"uid": "671f29fa10dda08074a4c6a341bb4f39",
"css": "bell-alt",
@ -273,4 +279,4 @@
]
}
]
}
}

View file

@ -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'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,7 @@
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -37,6 +37,7 @@
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -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
View 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">&#xe818;</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">&#xe819;</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">&#xe81a;</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">&#xe832;</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">&#xe81b;</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">&#xe832;</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">&#xe834;</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">&#xf08e;</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">&#xf08f;</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">&#xf0c9;</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">&#xf0c9;</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">&#xf0e0;</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">&#xf0e5;</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">&#xf0f3;</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">&#xf0fe;</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">&#xf0fe;</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">&#xf112;</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">&#xf13e;</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">&#xf141;</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">&#xf144;</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">&#xf144;</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">&#xf164;</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">&#xf1e5;</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">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>

Binary file not shown.

View file

@ -60,6 +60,8 @@
<glyph glyph-name="wrench" unicode="&#xe81a;" 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="&#xe81b;" 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="&#xe832;" 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="&#xe834;" 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.

View file

@ -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"]
]

View file

@ -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"]
]

View file

@ -1,10 +0,0 @@
[
"now",
["%ss", "%ss"],
["%smin", "%smin"],
["%sh", "%sh"],
["%sd", "%sd"],
["%sw", "%sw"],
["%smo", "%smo"],
["%sy", "%sy"]
]

View file

@ -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"]
]

View file

@ -1,10 +0,0 @@
[
"たった今",
"%s 秒前",
"%s 分前",
"%s 時間前",
"%s 日前",
"%s 週間前",
"%s ヶ月前",
"%s 年前"
]

View file

@ -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"]
]

View 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' })
})
})
})

1183
yarn.lock

File diff suppressed because it is too large Load diff