* upstream/develop: (193 commits) fix user avatar fallback logic remove dead code make bio textarea resizable vertically only remove dead code remove dead code fix crazy watch logic in conversation show three dot button only if needed hide mute conversation button to guests update keyBy generate idObj at timeline level fix pin showing logic in conversation Show a message when JS is disabled Initialize chat only if user is logged in and it wasn't initialized before i18n/Update Japanese i18n/Update pedantic Japanese sync profile tab state with location query refactor TabSwitcher use better name of controlled prop fix potential bug to render active tab in controlled way remove unused param ...emoji-mastoapi
commit
18ec13d796
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('autoprefixer')
|
||||||
|
]
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<conversation
|
<conversation
|
||||||
:collapsable="false"
|
:collapsable="false"
|
||||||
isPage="true"
|
is-page="true"
|
||||||
:statusoid="statusoid"
|
:statusoid="statusoid"
|
||||||
></conversation>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./conversation-page.js"></script>
|
<script src="./conversation-page.js"></script>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.dms')" v-bind:timeline="timeline" v-bind:timeline-name="'dms'"/>
|
<Timeline
|
||||||
|
:title="$t('nav.dms')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'dms'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./dm_timeline.js"></script>
|
<script src="./dm_timeline.js"></script>
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
import { debounce } from 'lodash'
|
||||||
|
/**
|
||||||
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
|
* data: object providing source information for specific types of suggestions:
|
||||||
|
* data.emoji - optional, an array of all emoji available i.e.
|
||||||
|
* (state.instance.emoji + state.instance.customEmoji)
|
||||||
|
* data.users - optional, an array of all known users
|
||||||
|
* updateUsersList - optional, a function to search and append to users
|
||||||
|
*
|
||||||
|
* Depending on data present one or both (or none) can be present, so if field
|
||||||
|
* doesn't support user linking you can just provide only emoji.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debounceUserSearch = debounce((data, input) => {
|
||||||
|
data.updateUsersList(input)
|
||||||
|
}, 500, { leading: true, trailing: false })
|
||||||
|
|
||||||
|
export default data => input => {
|
||||||
|
const firstChar = input[0]
|
||||||
|
if (firstChar === ':' && data.emoji) {
|
||||||
|
return suggestEmoji(data.emoji)(input)
|
||||||
|
}
|
||||||
|
if (firstChar === '@' && data.users) {
|
||||||
|
return suggestUsers(data)(input)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const suggestEmoji = emojis => input => {
|
||||||
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
|
return emojis
|
||||||
|
.filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
|
||||||
|
.sort((a, b) => {
|
||||||
|
let aScore = 0
|
||||||
|
let bScore = 0
|
||||||
|
|
||||||
|
// Make custom emojis a priority
|
||||||
|
aScore += a.imageUrl ? 10 : 0
|
||||||
|
bScore += b.imageUrl ? 10 : 0
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
const alphabetically = a.displayText > b.displayText ? 1 : -1
|
||||||
|
|
||||||
|
return bScore - aScore + alphabetically
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const suggestUsers = data => input => {
|
||||||
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
|
const users = data.users
|
||||||
|
|
||||||
|
const newUsers = users.filter(
|
||||||
|
user =>
|
||||||
|
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||||
|
user.name.toLowerCase().startsWith(noPrefix)
|
||||||
|
|
||||||
|
/* taking only 20 results so that sorting is a bit cheaper, we display
|
||||||
|
* only 5 anyway. could be inaccurate, but we ideally we should query
|
||||||
|
* backend anyway
|
||||||
|
*/
|
||||||
|
).slice(0, 20).sort((a, b) => {
|
||||||
|
let aScore = 0
|
||||||
|
let bScore = 0
|
||||||
|
|
||||||
|
// Matches on screen name (i.e. user@instance) makes a priority
|
||||||
|
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
|
||||||
|
// Matches on name takes second priority
|
||||||
|
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
|
||||||
|
const diff = (bScore - aScore) * 10
|
||||||
|
|
||||||
|
// Then sort alphabetically
|
||||||
|
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||||
|
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||||
|
|
||||||
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
}).map(({ screen_name, name, profile_image_url_original }) => ({
|
||||||
|
displayText: screen_name,
|
||||||
|
detailText: name,
|
||||||
|
imageUrl: profile_image_url_original,
|
||||||
|
replacement: '@' + screen_name + ' '
|
||||||
|
}))
|
||||||
|
|
||||||
|
// BE search users if there are no matches
|
||||||
|
if (newUsers.length === 0 && data.updateUsersList) {
|
||||||
|
debounceUserSearch(data, noPrefix)
|
||||||
|
}
|
||||||
|
return newUsers
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.timeline')" v-bind:timeline="timeline" v-bind:timeline-name="'friends'"/>
|
<Timeline
|
||||||
|
:title="$t('nav.timeline')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'friends'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./friends_timeline.js"></script>
|
<script src="./friends_timeline.js"></script>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.interactions')" v-bind:timeline="timeline" v-bind:timeline-name="'mentions'"/>
|
<Timeline
|
||||||
|
:title="$t('nav.interactions')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'mentions'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./mentions.js"></script>
|
<script src="./mentions.js"></script>
|
||||||
|
@ -1,42 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login panel panel-default">
|
<div class="login panel panel-default">
|
||||||
<!-- Default panel contents -->
|
<!-- Default panel contents -->
|
||||||
|
|
||||||
<div class="panel-heading">{{$t('login.heading.recovery')}}</div>
|
<div class="panel-heading">
|
||||||
|
{{ $t('login.heading.recovery') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form class='login-form' @submit.prevent='submit'>
|
<form
|
||||||
<div class='form-group'>
|
class="login-form"
|
||||||
<label for='code'>{{$t('login.recovery_code')}}</label>
|
@submit.prevent="submit"
|
||||||
<input v-model='code' class='form-control' id='code'>
|
>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="code">{{ $t('login.recovery_code') }}</label>
|
||||||
|
<input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='form-group'>
|
<div class="form-group">
|
||||||
<div class='login-bottom'>
|
<div class="login-bottom">
|
||||||
<div>
|
<div>
|
||||||
<a href="#" @click.prevent="requireTOTP">
|
<a
|
||||||
{{$t('login.enter_two_factor_code')}}
|
href="#"
|
||||||
|
@click.prevent="requireTOTP"
|
||||||
|
>
|
||||||
|
{{ $t('login.enter_two_factor_code') }}
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br>
|
||||||
<a href="#" @click.prevent="abortMFA">
|
<a
|
||||||
{{$t('general.cancel')}}
|
href="#"
|
||||||
|
@click.prevent="abortMFA"
|
||||||
|
>
|
||||||
|
{{ $t('general.cancel') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button type='submit' class='btn btn-default'>
|
<button
|
||||||
{{$t('general.verify')}}
|
type="submit"
|
||||||
|
class="btn btn-default"
|
||||||
|
>
|
||||||
|
{{ $t('general.verify') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class='form-group'>
|
<div
|
||||||
<div class='alert error'>
|
v-if="error"
|
||||||
{{error}}
|
class="form-group"
|
||||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
>
|
||||||
|
<div class="alert error">
|
||||||
|
{{ error }}
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="clearError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script src="./recovery_form.js" ></script>
|
<script src="./recovery_form.js" ></script>
|
||||||
|
@ -1,45 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login panel panel-default">
|
<div class="login panel panel-default">
|
||||||
<!-- Default panel contents -->
|
<!-- Default panel contents -->
|
||||||
|
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{{$t('login.heading.totp')}}
|
{{ $t('login.heading.totp') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form class='login-form' @submit.prevent='submit'>
|
<form
|
||||||
<div class='form-group'>
|
class="login-form"
|
||||||
<label for='code'>
|
@submit.prevent="submit"
|
||||||
{{$t('login.authentication_code')}}
|
>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="code">
|
||||||
|
{{ $t('login.authentication_code') }}
|
||||||
</label>
|
</label>
|
||||||
<input v-model='code' class='form-control' id='code'>
|
<input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='form-group'>
|
<div class="form-group">
|
||||||
<div class='login-bottom'>
|
<div class="login-bottom">
|
||||||
<div>
|
<div>
|
||||||
<a href="#" @click.prevent="requireRecovery">
|
<a
|
||||||
{{$t('login.enter_recovery_code')}}
|
href="#"
|
||||||
|
@click.prevent="requireRecovery"
|
||||||
|
>
|
||||||
|
{{ $t('login.enter_recovery_code') }}
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br>
|
||||||
<a href="#" @click.prevent="abortMFA">
|
<a
|
||||||
{{$t('general.cancel')}}
|
href="#"
|
||||||
|
@click.prevent="abortMFA"
|
||||||
|
>
|
||||||
|
{{ $t('general.cancel') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button type='submit' class='btn btn-default'>
|
<button
|
||||||
{{$t('general.verify')}}
|
type="submit"
|
||||||
|
class="btn btn-default"
|
||||||
|
>
|
||||||
|
{{ $t('general.verify') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class='form-group'>
|
<div
|
||||||
<div class='alert error'>
|
v-if="error"
|
||||||
{{error}}
|
class="form-group"
|
||||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
>
|
||||||
|
<div class="alert error">
|
||||||
|
{{ error }}
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="clearError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script src="./totp_form.js"></script>
|
<script src="./totp_form.js"></script>
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import { forEach, map } from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Poll',
|
||||||
|
props: ['basePoll'],
|
||||||
|
components: { Timeago },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
choices: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (!this.$store.state.polls.pollsObject[this.pollId]) {
|
||||||
|
this.$store.dispatch('mergeOrAddPoll', this.basePoll)
|
||||||
|
}
|
||||||
|
this.$store.dispatch('trackPoll', this.pollId)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.dispatch('untrackPoll', this.pollId)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pollId () {
|
||||||
|
return this.basePoll.id
|
||||||
|
},
|
||||||
|
poll () {
|
||||||
|
const storePoll = this.$store.state.polls.pollsObject[this.pollId]
|
||||||
|
return storePoll || {}
|
||||||
|
},
|
||||||
|
options () {
|
||||||
|
return (this.poll && this.poll.options) || []
|
||||||
|
},
|
||||||
|
expiresAt () {
|
||||||
|
return (this.poll && this.poll.expires_at) || 0
|
||||||
|
},
|
||||||
|
expired () {
|
||||||
|
return (this.poll && this.poll.expired) || false
|
||||||
|
},
|
||||||
|
loggedIn () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
showResults () {
|
||||||
|
return this.poll.voted || this.expired || !this.loggedIn
|
||||||
|
},
|
||||||
|
totalVotesCount () {
|
||||||
|
return this.poll.votes_count
|
||||||
|
},
|
||||||
|
containerClass () {
|
||||||
|
return {
|
||||||
|
loading: this.loading
|
||||||
|
}
|
||||||
|
},
|
||||||
|
choiceIndices () {
|
||||||
|
// Convert array of booleans into an array of indices of the
|
||||||
|
// items that were 'true', so [true, false, false, true] becomes
|
||||||
|
// [0, 3].
|
||||||
|
return this.choices
|
||||||
|
.map((entry, index) => entry && index)
|
||||||
|
.filter(value => typeof value === 'number')
|
||||||
|
},
|
||||||
|
isDisabled () {
|
||||||
|
const noChoice = this.choiceIndices.length === 0
|
||||||
|
return this.loading || noChoice
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
percentageForOption (count) {
|
||||||
|
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
|
||||||
|
},
|
||||||
|
resultTitle (option) {
|
||||||
|
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
|
||||||
|
},
|
||||||
|
fetchPoll () {
|
||||||
|
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
|
||||||
|
},
|
||||||
|
activateOption (index) {
|
||||||
|
// forgive me father: doing checking the radio/checkboxes
|
||||||
|
// in code because of customized input elements need either
|
||||||
|
// a) an extra element for the actual graphic, or b) use a
|
||||||
|
// pseudo element for the label. We use b) which mandates
|
||||||
|
// using "for" and "id" matching which isn't nice when the
|
||||||
|
// same poll appears multiple times on the site (notifs and
|
||||||
|
// timeline for example). With code we can make sure it just
|
||||||
|
// works without altering the pseudo element implementation.
|
||||||
|
const allElements = this.$el.querySelectorAll('input')
|
||||||
|
const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
|
||||||
|
if (this.poll.multiple) {
|
||||||
|
// Checkboxes, toggle only the clicked one
|
||||||
|
clickedElement.checked = !clickedElement.checked
|
||||||
|
} else {
|
||||||
|
// Radio button, uncheck everything and check the clicked one
|
||||||
|
forEach(allElements, element => { element.checked = false })
|
||||||
|
clickedElement.checked = true
|
||||||
|
}
|
||||||
|
this.choices = map(allElements, e => e.checked)
|
||||||
|
},
|
||||||
|
optionId (index) {
|
||||||
|
return `poll${this.poll.id}-${index}`
|
||||||
|
},
|
||||||
|
vote () {
|
||||||
|
if (this.choiceIndices.length === 0) return
|
||||||
|
this.loading = true
|
||||||
|
this.$store.dispatch(
|
||||||
|
'votePoll',
|
||||||
|
{ id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
|
||||||
|
).then(poll => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="poll"
|
||||||
|
:class="containerClass"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
class="poll-option"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
v-else
|
||||||
|
@click="activateOption(index)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="poll.multiple"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="loading"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="radio"
|
||||||
|
:disabled="loading"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
<label class="option-vote">
|
||||||
|
<div>{{ option.title }}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer faint">
|
||||||
|
<button
|
||||||
|
v-if="!showResults"
|
||||||
|
class="btn btn-default poll-vote-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
@click="vote"
|
||||||
|
>
|
||||||
|
{{ $t('polls.vote') }}
|
||||||
|
</button>
|
||||||
|
<div class="total">
|
||||||
|
{{ totalVotesCount }} {{ $t("polls.votes") }} ·
|
||||||
|
</div>
|
||||||
|
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
|
||||||
|
<Timeago
|
||||||
|
:time="expiresAt"
|
||||||
|
:auto-update="60"
|
||||||
|
:now-threshold="0"
|
||||||
|
/>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./poll.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
.votes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
.poll-option {
|
||||||
|
margin: 0.75em 0.5em;
|
||||||
|
}
|
||||||
|
.option-result {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: relative;
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
.option-result-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.1em 0.25em;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.result-percentage {
|
||||||
|
width: 3.5em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.result-fill {
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--linkBg, $fallback--lightBg);
|
||||||
|
border-radius: $fallback--panelRadius;
|
||||||
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
.option-vote {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 3.5em;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&.loading * {
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
.poll-vote-button {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="poll-form"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
class="poll-option"
|
||||||
|
>
|
||||||
|
<div class="input-container">
|
||||||
|
<input
|
||||||
|
:id="`poll-${index}`"
|
||||||
|
v-model="options[index]"
|
||||||
|
class="poll-option-input"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('polls.option')"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
@change="updatePollToParent"
|
||||||
|
@keydown.enter.stop.prevent="nextOption(index)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="options.length > 2"
|
||||||
|
class="icon-container"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon-cancel"
|
||||||
|
@click="deleteOption(index)"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
v-model="pollType"
|
||||||
|
class="select"
|
||||||
|
@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
|
||||||
|
v-model="expiryAmount"
|
||||||
|
type="number"
|
||||||
|
class="expiry-amount hide-number-spinner"
|
||||||
|
:min="minExpirationInCurrentUnit"
|
||||||
|
:max="maxExpirationInCurrentUnit"
|
||||||
|
@change="expiryAmountChange"
|
||||||
|
>
|
||||||
|
<label class="expiry-unit select">
|
||||||
|
<select
|
||||||
|
v-model="expiryUnit"
|
||||||
|
@change="expiryAmountChange"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="unit in expiryUnits"
|
||||||
|
:key="unit"
|
||||||
|
: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>
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
|
<Timeline
|
||||||
|
:title="$t('nav.twkn')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'publicAndExternal'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./public_and_external_timeline.js"></script>
|
<script src="./public_and_external_timeline.js"></script>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.public_tl')" v-bind:timeline="timeline" v-bind:timeline-name="'public'"/>
|
<Timeline
|
||||||
|
:title="$t('nav.public_tl')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'public'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./public_timeline.js"></script>
|
<script src="./public_timeline.js"></script>
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import FollowCard from '../follow_card/follow_card.vue'
|
||||||
|
import Conversation from '../conversation/conversation.vue'
|
||||||
|
import Status from '../status/status.vue'
|
||||||
|
import map from 'lodash/map'
|
||||||
|
|
||||||
|
const Search = {
|
||||||
|
components: {
|
||||||
|
FollowCard,
|
||||||
|
Conversation,
|
||||||
|
Status
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'query'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
searchTerm: this.query || '',
|
||||||
|
userIds: [],
|
||||||
|
statuses: [],
|
||||||
|
hashtags: [],
|
||||||
|
currenResultTab: 'statuses'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users () {
|
||||||
|
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||||
|
},
|
||||||
|
visibleStatuses () {
|
||||||
|
const allStatusesObject = this.$store.state.statuses.allStatusesObject
|
||||||
|
|
||||||
|
return this.statuses.filter(status =>
|
||||||
|
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.search(this.query)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
query (newValue) {
|
||||||
|
this.searchTerm = newValue
|
||||||
|
this.search(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
newQuery (query) {
|
||||||
|
this.$router.push({ name: 'search', query: { query } })
|
||||||
|
this.$refs.searchInput.focus()
|
||||||
|
},
|
||||||
|
search (query) {
|
||||||
|
if (!query) {
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.userIds = []
|
||||||
|
this.statuses = []
|
||||||
|
this.hashtags = []
|
||||||
|
this.$refs.searchInput.blur()
|
||||||
|
|
||||||
|
this.$store.dispatch('search', { q: query, resolve: true })
|
||||||
|
.then(data => {
|
||||||
|
this.loading = false
|
||||||
|
this.userIds = map(data.accounts, 'id')
|
||||||
|
this.statuses = data.statuses
|
||||||
|
this.hashtags = data.hashtags
|
||||||
|
this.currenResultTab = this.getActiveTab()
|
||||||
|
this.loaded = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resultCount (tabName) {
|
||||||
|
const length = this[tabName].length
|
||||||
|
return length === 0 ? '' : ` (${length})`
|
||||||
|
},
|
||||||
|
onResultTabSwitch (key) {
|
||||||
|
this.currenResultTab = key
|
||||||
|
},
|
||||||
|
getActiveTab () {
|
||||||
|
if (this.visibleStatuses.length > 0) {
|
||||||
|
return 'statuses'
|
||||||
|
} else if (this.users.length > 0) {
|
||||||
|
return 'people'
|
||||||
|
} else if (this.hashtags.length > 0) {
|
||||||
|
return 'hashtags'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'statuses'
|
||||||
|
},
|
||||||
|
lastHistoryRecord (hashtag) {
|
||||||
|
return hashtag.history && hashtag.history[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search
|
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t('nav.search') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search-input-container">
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="$t('nav.search')"
|
||||||
|
@keyup.enter="newQuery(searchTerm)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn search-button"
|
||||||
|
@click="newQuery(searchTerm)"
|
||||||
|
>
|
||||||
|
<i class="icon-search" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="text-center loading-icon"
|
||||||
|
>
|
||||||
|
<i class="icon-spin3 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loaded">
|
||||||
|
<div class="search-nav-heading">
|
||||||
|
<tab-switcher
|
||||||
|
ref="tabSwitcher"
|
||||||
|
:on-switch="onResultTabSwitch"
|
||||||
|
:active-tab="currenResultTab"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
key="statuses"
|
||||||
|
:label="$t('user_card.statuses') + resultCount('visibleStatuses')"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
key="people"
|
||||||
|
:label="$t('search.people') + resultCount('users')"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
key="hashtags"
|
||||||
|
:label="$t('search.hashtags') + resultCount('hashtags')"
|
||||||
|
/>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div v-if="currenResultTab === 'statuses'">
|
||||||
|
<div
|
||||||
|
v-if="visibleStatuses.length === 0 && !loading && loaded"
|
||||||
|
class="search-result-heading"
|
||||||
|
>
|
||||||
|
<h4>{{ $t('search.no_results') }}</h4>
|
||||||
|
</div>
|
||||||
|
<Status
|
||||||
|
v-for="status in visibleStatuses"
|
||||||
|
:key="status.id"
|
||||||
|
:collapsable="false"
|
||||||
|
:expandable="false"
|
||||||
|
:compact="false"
|
||||||
|
class="search-result"
|
||||||
|
:statusoid="status"
|
||||||
|
:no-heading="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currenResultTab === 'people'">
|
||||||
|
<div
|
||||||
|
v-if="users.length === 0 && !loading && loaded"
|
||||||
|
class="search-result-heading"
|
||||||
|
>
|
||||||
|
<h4>{{ $t('search.no_results') }}</h4>
|
||||||
|
</div>
|
||||||
|
<FollowCard
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:user="user"
|
||||||
|
class="list-item search-result"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currenResultTab === 'hashtags'">
|
||||||
|
<div
|
||||||
|
v-if="hashtags.length === 0 && !loading && loaded"
|
||||||
|
class="search-result-heading"
|
||||||
|
>
|
||||||
|
<h4>{{ $t('search.no_results') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="hashtag in hashtags"
|
||||||
|
:key="hashtag.url"
|
||||||
|
class="status trend search-result"
|
||||||
|
>
|
||||||
|
<div class="hashtag">
|
||||||
|
<router-link :to="{ name: 'tag-timeline', params: { tag: hashtag.name } }">
|
||||||
|
#{{ hashtag.name }}
|
||||||
|
</router-link>
|
||||||
|
<div v-if="lastHistoryRecord(hashtag)">
|
||||||
|
<span v-if="lastHistoryRecord(hashtag).accounts == 1">
|
||||||
|
{{ $t('search.person_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ $t('search.people_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="lastHistoryRecord(hashtag)"
|
||||||
|
class="count"
|
||||||
|
>
|
||||||
|
{{ lastHistoryRecord(hashtag).uses }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search-result-footer text-center panel-footer faint" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./search.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.search-result-heading {
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
.search-nav-heading {
|
||||||
|
.tab-switcher .tabs .tab-wrapper {
|
||||||
|
display: block;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-footer {
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
padding: 10px;
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--panel, $fallback--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
padding: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.hashtag {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue