Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: review lint fix fix all known problems with clicks on autocomplete emojis fix specificity that made attachments misalign Keep statuses always enabled add resolve param to user search api request change isPinned to noIdUpdate Fix: problems with polls state Move character counter into the input box fix for #553 Make scss change for tab switcher only delete state.token instead of setting false use clientSecret in login flow fix error breaking logout flow make sure to clear old token when logout Move poll state handling to its own module Fix/messed up long polls A small sass fix for #577 reset margin property of form controls
This commit is contained in:
commit
6aa57d377d
20 changed files with 183 additions and 60 deletions
|
@ -131,6 +131,7 @@ input, textarea, .select {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--inputFont, sans-serif);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 8px .5em;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
|
@ -199,6 +200,7 @@ input, textarea, .select {
|
|||
}
|
||||
}
|
||||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
transition: box-shadow 200ms;
|
||||
|
@ -235,6 +237,7 @@ input, textarea, .select {
|
|||
}
|
||||
}
|
||||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '✔';
|
||||
transition: color 200ms;
|
||||
|
|
|
@ -59,7 +59,8 @@ const EmojiInput = {
|
|||
input: undefined,
|
||||
highlighted: 0,
|
||||
caret: 0,
|
||||
focused: false
|
||||
focused: false,
|
||||
blurTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -122,12 +123,12 @@ const EmojiInput = {
|
|||
this.$emit('input', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
replaceText (e) {
|
||||
replaceText (e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (this.textAtCaret.length === 1) { return }
|
||||
if (len > 0) {
|
||||
const suggestion = this.suggestions[this.highlighted]
|
||||
const replacement = suggestion.replacement
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||
this.$emit('input', newValue)
|
||||
this.highlighted = 0
|
||||
|
@ -173,13 +174,21 @@ const EmojiInput = {
|
|||
onBlur (e) {
|
||||
// Clicking on any suggestion removes focus from autocomplete,
|
||||
// preventing click handler ever executing.
|
||||
setTimeout(() => {
|
||||
this.blurTimeout = setTimeout(() => {
|
||||
this.focused = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
}, 200)
|
||||
},
|
||||
onClick (e, suggestion) {
|
||||
this.replaceText(e, suggestion)
|
||||
},
|
||||
onFocus (e) {
|
||||
if (this.blurTimeout) {
|
||||
clearTimeout(this.blurTimeout)
|
||||
this.blurTimeout = null
|
||||
}
|
||||
|
||||
this.focused = true
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
@click.stop.prevent="replaceText"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
class="autocomplete-item"
|
||||
:class="{ highlighted: suggestion.highlighted }"
|
||||
>
|
||||
|
|
|
@ -28,7 +28,9 @@
|
|||
flex-grow: 1;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.attachments, .attachment {
|
||||
// FIXME: specificity problem with this and .attachments.attachment
|
||||
// we shouldn't have the need for .image here
|
||||
.attachment.image {
|
||||
margin: 0 0.5em 0 0;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
|
|
|
@ -26,9 +26,10 @@ const LoginForm = {
|
|||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken () {
|
||||
const { clientId } = this.oauth
|
||||
const { clientId, clientSecret } = this.oauth
|
||||
const data = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@ const oac = {
|
|||
props: ['code'],
|
||||
mounted () {
|
||||
if (this.code) {
|
||||
const { clientId } = this.$store.state.oauth
|
||||
const { clientId, clientSecret } = this.$store.state.oauth
|
||||
|
||||
oauth.getToken({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance: this.$store.state.instance.server,
|
||||
code: this.code
|
||||
}).then((result) => {
|
||||
|
|
|
@ -3,26 +3,39 @@ import { forEach, map } from 'lodash'
|
|||
|
||||
export default {
|
||||
name: 'Poll',
|
||||
props: ['poll', 'statusId'],
|
||||
props: ['basePoll'],
|
||||
components: { Timeago },
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
choices: [],
|
||||
refreshInterval: null
|
||||
choices: []
|
||||
}
|
||||
},
|
||||
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)
|
||||
if (!this.$store.state.polls.pollsObject[this.pollId]) {
|
||||
this.$store.dispatch('mergeOrAddPoll', this.basePoll)
|
||||
}
|
||||
this.$store.dispatch('trackPoll', this.pollId)
|
||||
},
|
||||
destroyed () {
|
||||
clearTimeout(this.refreshInterval)
|
||||
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 Date.now() > Date.parse(this.poll.expires_at)
|
||||
return (this.poll && this.poll.expired) || false
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
|
@ -33,9 +46,6 @@ export default {
|
|||
totalVotesCount () {
|
||||
return this.poll.votes_count
|
||||
},
|
||||
expiresAt () {
|
||||
return Date.parse(this.poll.expires_at).toLocaleString()
|
||||
},
|
||||
containerClass () {
|
||||
return {
|
||||
loading: this.loading
|
||||
|
@ -55,11 +65,6 @@ export default {
|
|||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
|
@ -104,4 +109,4 @@ export default {
|
|||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="poll" v-bind:class="containerClass">
|
||||
<div
|
||||
class="poll-option"
|
||||
v-for="(option, index) in poll.options"
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
>
|
||||
<div v-if="showResults" :title="resultTitle(option)" class="option-result">
|
||||
|
@ -31,8 +31,8 @@
|
|||
:disabled="loading"
|
||||
:value="index"
|
||||
>
|
||||
<label>
|
||||
{{option.title}}
|
||||
<label class="option-vote">
|
||||
<div>{{option.title}}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,7 +50,7 @@
|
|||
{{totalVotesCount}} {{ $t("polls.votes") }} ·
|
||||
</div>
|
||||
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
|
||||
<Timeago :time="this.poll.expires_at" :auto-update="60" :now-threshold="0" />
|
||||
<Timeago :time="this.expiresAt" :auto-update="60" :now-threshold="0" />
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,8 +68,7 @@
|
|||
margin: 0 0 0.5em;
|
||||
}
|
||||
.poll-option {
|
||||
margin: 0.5em 0;
|
||||
height: 1.5em;
|
||||
margin: 0.75em 0.5em;
|
||||
}
|
||||
.option-result {
|
||||
height: 100%;
|
||||
|
@ -87,6 +86,7 @@
|
|||
}
|
||||
.result-percentage {
|
||||
width: 3.5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.result-fill {
|
||||
height: 100%;
|
||||
|
@ -99,6 +99,10 @@
|
|||
left: 0;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
.option-vote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
input {
|
||||
width: 3.5em;
|
||||
}
|
||||
|
|
|
@ -269,8 +269,11 @@ const PostStatusForm = {
|
|||
resize (e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
|
||||
Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
|
||||
const topPaddingStr = window.getComputedStyle(target)['padding-top']
|
||||
const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
|
||||
// Remove "px" at the end of the values
|
||||
const vertPadding = Number(topPaddingStr.substr(0, topPaddingStr.length - 2)) +
|
||||
Number(bottomPaddingStr.substr(0, bottomPaddingStr.length - 2))
|
||||
// Auto is needed to make textbox shrink when removing lines
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${target.scrollHeight - vertPadding}px`
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
<EmojiInput
|
||||
:suggest="emojiUserSuggestor"
|
||||
v-model="newStatus.status"
|
||||
class="form-control"
|
||||
class="form-control main-input"
|
||||
>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
|
@ -65,6 +65,13 @@
|
|||
class="form-post-body"
|
||||
>
|
||||
</textarea>
|
||||
<p
|
||||
v-if="hasStatusLengthLimit"
|
||||
class="character-counter faint"
|
||||
:class="{ error: isOverLengthLimit }"
|
||||
>
|
||||
{{ charactersLeft }}
|
||||
</p>
|
||||
</EmojiInput>
|
||||
<div class="visibility-tray">
|
||||
<div class="text-format" v-if="postFormats.length > 1">
|
||||
|
@ -109,8 +116,6 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -304,10 +309,12 @@
|
|||
}
|
||||
|
||||
.form-post-body {
|
||||
line-height:16px;
|
||||
height: 16px; // Only affects the empty-height
|
||||
line-height: 16px;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
transition: min-height 200ms 100ms;
|
||||
padding-bottom: 1.75em;
|
||||
min-height: 1px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
@ -316,6 +323,23 @@
|
|||
min-height: 48px;
|
||||
}
|
||||
|
||||
.main-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0 0.5em;
|
||||
|
||||
&.error {
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :poll="status.poll" :status-id="status.id" />
|
||||
<poll :base-poll="status.poll" />
|
||||
</div>
|
||||
|
||||
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div v-if="user" class="user-profile panel panel-default">
|
||||
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
|
||||
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
|
||||
<div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
|
||||
<div :label="$t('user_card.statuses')">
|
||||
<div class="timeline">
|
||||
<template v-for="statusId in user.pinnedStatuseIds">
|
||||
<Conversation
|
||||
|
|
|
@ -19,7 +19,8 @@ const saveImmedeatelyActions = [
|
|||
'setHighlight',
|
||||
'setOption',
|
||||
'setClientData',
|
||||
'setToken'
|
||||
'setToken',
|
||||
'clearToken'
|
||||
]
|
||||
|
||||
const defaultStorage = (() => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import authFlowModule from './modules/auth_flow.js'
|
|||
import mediaViewerModule from './modules/media_viewer.js'
|
||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
import pollsModule from './modules/polls.js'
|
||||
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
|
@ -72,7 +73,8 @@ const persistedStateOptions = {
|
|||
authFlow: authFlowModule,
|
||||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule,
|
||||
reports: reportsModule
|
||||
reports: reportsModule,
|
||||
polls: pollsModule
|
||||
},
|
||||
plugins: [persistedState, pushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
|
|
|
@ -21,7 +21,7 @@ const chat = {
|
|||
},
|
||||
actions: {
|
||||
disconnectFromChat (store) {
|
||||
store.state.socket.disconnect()
|
||||
store.state.socket && store.state.socket.disconnect()
|
||||
},
|
||||
initializeChat (store, socket) {
|
||||
const channel = socket.channel('chat:public')
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { delete as del } from 'vue'
|
||||
|
||||
const oauth = {
|
||||
state: {
|
||||
clientId: false,
|
||||
|
@ -22,6 +24,12 @@ const oauth = {
|
|||
},
|
||||
setToken (state, token) {
|
||||
state.userToken = token
|
||||
},
|
||||
clearToken (state) {
|
||||
state.userToken = false
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// let's clear it as well, since it is being used as a fallback of state.userToken
|
||||
del(state, 'token')
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
|
70
src/modules/polls.js
Normal file
70
src/modules/polls.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { merge } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
|
||||
const polls = {
|
||||
state: {
|
||||
// Contains key = id, value = number of trackers for this poll
|
||||
trackedPolls: {},
|
||||
pollsObject: {}
|
||||
},
|
||||
mutations: {
|
||||
mergeOrAddPoll (state, poll) {
|
||||
const existingPoll = state.pollsObject[poll.id]
|
||||
// Make expired-state change trigger re-renders properly
|
||||
poll.expired = Date.now() > Date.parse(poll.expires_at)
|
||||
if (existingPoll) {
|
||||
set(state.pollsObject, poll.id, merge(existingPoll, poll))
|
||||
} else {
|
||||
set(state.pollsObject, poll.id, poll)
|
||||
}
|
||||
},
|
||||
trackPoll (state, pollId) {
|
||||
const currentValue = state.trackedPolls[pollId]
|
||||
if (currentValue) {
|
||||
set(state.trackedPolls, pollId, currentValue + 1)
|
||||
} else {
|
||||
set(state.trackedPolls, pollId, 1)
|
||||
}
|
||||
},
|
||||
untrackPoll (state, pollId) {
|
||||
const currentValue = state.trackedPolls[pollId]
|
||||
if (currentValue) {
|
||||
set(state.trackedPolls, pollId, currentValue - 1)
|
||||
} else {
|
||||
set(state.trackedPolls, pollId, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
mergeOrAddPoll ({ commit }, poll) {
|
||||
commit('mergeOrAddPoll', poll)
|
||||
},
|
||||
updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
|
||||
rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
|
||||
setTimeout(() => {
|
||||
if (rootState.polls.trackedPolls[pollId]) {
|
||||
dispatch('updateTrackedPoll', pollId)
|
||||
}
|
||||
}, 30 * 1000)
|
||||
commit('mergeOrAddPoll', poll)
|
||||
})
|
||||
},
|
||||
trackPoll ({ rootState, commit, dispatch }, pollId) {
|
||||
if (!rootState.polls.trackedPolls[pollId]) {
|
||||
setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
|
||||
}
|
||||
commit('trackPoll', pollId)
|
||||
},
|
||||
untrackPoll ({ commit }, pollId) {
|
||||
commit('untrackPoll', pollId)
|
||||
},
|
||||
votePoll ({ rootState, commit }, { id, pollId, choices }) {
|
||||
return rootState.api.backendInteractor.vote(pollId, choices).then(poll => {
|
||||
commit('mergeOrAddPoll', poll)
|
||||
return poll
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default polls
|
|
@ -146,7 +146,8 @@ const removeStatusFromGlobalStorage = (state, status) => {
|
|||
}
|
||||
}
|
||||
|
||||
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
|
||||
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
|
||||
noIdUpdate = false, userId }) => {
|
||||
// Sanity check
|
||||
if (!isArray(statuses)) {
|
||||
return false
|
||||
|
@ -543,7 +544,7 @@ const statuses = {
|
|||
},
|
||||
fetchPinnedStatuses ({ rootState, dispatch }, userId) {
|
||||
rootState.api.backendInteractor.fetchPinnedStatuses(userId)
|
||||
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true }))
|
||||
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
|
||||
},
|
||||
pinStatus ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.pinOwnStatus(statusId)
|
||||
|
@ -582,18 +583,6 @@ 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
|
||||
|
|
|
@ -399,7 +399,7 @@ const users = {
|
|||
logout (store) {
|
||||
store.commit('clearCurrentUser')
|
||||
store.dispatch('disconnectFromChat')
|
||||
store.commit('setToken', false)
|
||||
store.commit('clearToken')
|
||||
store.dispatch('stopFetching', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
store.dispatch('stopFetching', 'notifications')
|
||||
|
|
|
@ -6,7 +6,8 @@ const search = ({query, store}) => {
|
|||
store,
|
||||
url: '/api/v1/accounts/search',
|
||||
params: {
|
||||
q: query
|
||||
q: query,
|
||||
resolve: true
|
||||
}
|
||||
})
|
||||
.then((data) => data.json())
|
||||
|
|
Loading…
Add table
Reference in a new issue