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:
Henry Jameson 2019-06-26 00:40:37 +03:00
commit 6aa57d377d
20 changed files with 183 additions and 60 deletions

View file

@ -131,6 +131,7 @@ input, textarea, .select {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--inputFont, sans-serif); font-family: var(--inputFont, sans-serif);
font-size: 14px; font-size: 14px;
margin: 0;
padding: 8px .5em; padding: 8px .5em;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
@ -199,6 +200,7 @@ input, textarea, .select {
} }
} }
+ label::before { + label::before {
flex-shrink: 0;
display: inline-block; display: inline-block;
content: ''; content: '';
transition: box-shadow 200ms; transition: box-shadow 200ms;
@ -235,6 +237,7 @@ input, textarea, .select {
} }
} }
+ label::before { + label::before {
flex-shrink: 0;
display: inline-block; display: inline-block;
content: ''; content: '';
transition: color 200ms; transition: color 200ms;

View file

@ -59,7 +59,8 @@ const EmojiInput = {
input: undefined, input: undefined,
highlighted: 0, highlighted: 0,
caret: 0, caret: 0,
focused: false focused: false,
blurTimeout: null
} }
}, },
computed: { computed: {
@ -122,12 +123,12 @@ const EmojiInput = {
this.$emit('input', newValue) this.$emit('input', newValue)
this.caret = 0 this.caret = 0
}, },
replaceText (e) { replaceText (e, suggestion) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (this.textAtCaret.length === 1) { return } if (this.textAtCaret.length === 1) { return }
if (len > 0) { if (len > 0 || suggestion) {
const suggestion = this.suggestions[this.highlighted] const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
const replacement = suggestion.replacement const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('input', newValue) this.$emit('input', newValue)
this.highlighted = 0 this.highlighted = 0
@ -173,13 +174,21 @@ const EmojiInput = {
onBlur (e) { onBlur (e) {
// Clicking on any suggestion removes focus from autocomplete, // Clicking on any suggestion removes focus from autocomplete,
// preventing click handler ever executing. // preventing click handler ever executing.
setTimeout(() => { this.blurTimeout = setTimeout(() => {
this.focused = false this.focused = false
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()
}, 200) }, 200)
}, },
onClick (e, suggestion) {
this.replaceText(e, suggestion)
},
onFocus (e) { onFocus (e) {
if (this.blurTimeout) {
clearTimeout(this.blurTimeout)
this.blurTimeout = null
}
this.focused = true this.focused = true
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()

View file

@ -6,7 +6,7 @@
<div <div
v-for="(suggestion, index) in suggestions" v-for="(suggestion, index) in suggestions"
:key="index" :key="index"
@click.stop.prevent="replaceText" @click.stop.prevent="onClick($event, suggestion)"
class="autocomplete-item" class="autocomplete-item"
:class="{ highlighted: suggestion.highlighted }" :class="{ highlighted: suggestion.highlighted }"
> >

View file

@ -28,7 +28,9 @@
flex-grow: 1; flex-grow: 1;
margin-top: 0.5em; 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; margin: 0 0.5em 0 0;
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;

View file

@ -26,9 +26,10 @@ const LoginForm = {
this.isTokenAuth ? this.submitToken() : this.submitPassword() this.isTokenAuth ? this.submitToken() : this.submitPassword()
}, },
submitToken () { submitToken () {
const { clientId } = this.oauth const { clientId, clientSecret } = this.oauth
const data = { const data = {
clientId, clientId,
clientSecret,
instance: this.instance.server, instance: this.instance.server,
commit: this.$store.commit commit: this.$store.commit
} }

View file

@ -4,10 +4,11 @@ const oac = {
props: ['code'], props: ['code'],
mounted () { mounted () {
if (this.code) { if (this.code) {
const { clientId } = this.$store.state.oauth const { clientId, clientSecret } = this.$store.state.oauth
oauth.getToken({ oauth.getToken({
clientId, clientId,
clientSecret,
instance: this.$store.state.instance.server, instance: this.$store.state.instance.server,
code: this.code code: this.code
}).then((result) => { }).then((result) => {

View file

@ -3,26 +3,39 @@ import { forEach, map } from 'lodash'
export default { export default {
name: 'Poll', name: 'Poll',
props: ['poll', 'statusId'], props: ['basePoll'],
components: { Timeago }, components: { Timeago },
data () { data () {
return { return {
loading: false, loading: false,
choices: [], choices: []
refreshInterval: null
} }
}, },
created () { created () {
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000) if (!this.$store.state.polls.pollsObject[this.pollId]) {
// Initialize choices to booleans and set its length to match options this.$store.dispatch('mergeOrAddPoll', this.basePoll)
this.choices = this.poll.options.map(_ => false) }
this.$store.dispatch('trackPoll', this.pollId)
}, },
destroyed () { destroyed () {
clearTimeout(this.refreshInterval) this.$store.dispatch('untrackPoll', this.pollId)
}, },
computed: { 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 () { expired () {
return Date.now() > Date.parse(this.poll.expires_at) return (this.poll && this.poll.expired) || false
}, },
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
@ -33,9 +46,6 @@ export default {
totalVotesCount () { totalVotesCount () {
return this.poll.votes_count return this.poll.votes_count
}, },
expiresAt () {
return Date.parse(this.poll.expires_at).toLocaleString()
},
containerClass () { containerClass () {
return { return {
loading: this.loading loading: this.loading
@ -55,11 +65,6 @@ export default {
} }
}, },
methods: { methods: {
refreshPoll () {
if (this.expired) return
this.fetchPoll()
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
},
percentageForOption (count) { percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100) return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
}, },
@ -104,4 +109,4 @@ export default {
}) })
} }
} }
} }

View file

@ -2,7 +2,7 @@
<div class="poll" v-bind:class="containerClass"> <div class="poll" v-bind:class="containerClass">
<div <div
class="poll-option" class="poll-option"
v-for="(option, index) in poll.options" v-for="(option, index) in options"
:key="index" :key="index"
> >
<div v-if="showResults" :title="resultTitle(option)" class="option-result"> <div v-if="showResults" :title="resultTitle(option)" class="option-result">
@ -31,8 +31,8 @@
:disabled="loading" :disabled="loading"
:value="index" :value="index"
> >
<label> <label class="option-vote">
{{option.title}} <div>{{option.title}}</div>
</label> </label>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@
{{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp; {{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp;
</div> </div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <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> </i18n>
</div> </div>
</div> </div>
@ -68,8 +68,7 @@
margin: 0 0 0.5em; margin: 0 0 0.5em;
} }
.poll-option { .poll-option {
margin: 0.5em 0; margin: 0.75em 0.5em;
height: 1.5em;
} }
.option-result { .option-result {
height: 100%; height: 100%;
@ -87,6 +86,7 @@
} }
.result-percentage { .result-percentage {
width: 3.5em; width: 3.5em;
flex-shrink: 0;
} }
.result-fill { .result-fill {
height: 100%; height: 100%;
@ -99,6 +99,10 @@
left: 0; left: 0;
transition: width 0.5s; transition: width 0.5s;
} }
.option-vote {
display: flex;
align-items: center;
}
input { input {
width: 3.5em; width: 3.5em;
} }

View file

@ -269,8 +269,11 @@ const PostStatusForm = {
resize (e) { resize (e) {
const target = e.target || e const target = e.target || e
if (!(target instanceof window.Element)) { return } if (!(target instanceof window.Element)) { return }
const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + const topPaddingStr = window.getComputedStyle(target)['padding-top']
Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) 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 // Auto is needed to make textbox shrink when removing lines
target.style.height = 'auto' target.style.height = 'auto'
target.style.height = `${target.scrollHeight - vertPadding}px` target.style.height = `${target.scrollHeight - vertPadding}px`

View file

@ -48,7 +48,7 @@
<EmojiInput <EmojiInput
:suggest="emojiUserSuggestor" :suggest="emojiUserSuggestor"
v-model="newStatus.status" v-model="newStatus.status"
class="form-control" class="form-control main-input"
> >
<textarea <textarea
ref="textarea" ref="textarea"
@ -65,6 +65,13 @@
class="form-post-body" class="form-post-body"
> >
</textarea> </textarea>
<p
v-if="hasStatusLengthLimit"
class="character-counter faint"
:class="{ error: isOverLengthLimit }"
>
{{ charactersLeft }}
</p>
</EmojiInput> </EmojiInput>
<div class="visibility-tray"> <div class="visibility-tray">
<div class="text-format" v-if="postFormats.length > 1"> <div class="text-format" v-if="postFormats.length > 1">
@ -109,8 +116,6 @@
/> />
</div> </div>
</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-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-if="isOverLengthLimit" disabled class="btn btn-default">{{$t('general.submit')}}</button>
@ -304,10 +309,12 @@
} }
.form-post-body { .form-post-body {
line-height:16px; height: 16px; // Only affects the empty-height
line-height: 16px;
resize: none; resize: none;
overflow: hidden; overflow: hidden;
transition: min-height 200ms 100ms; transition: min-height 200ms 100ms;
padding-bottom: 1.75em;
min-height: 1px; min-height: 1px;
box-sizing: content-box; box-sizing: content-box;
} }
@ -316,6 +323,23 @@
min-height: 48px; 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 { .btn {
cursor: pointer; cursor: pointer;
} }

View file

@ -124,7 +124,7 @@
</div> </div>
<div v-if="status.poll && status.poll.options"> <div v-if="status.poll && status.poll.options">
<poll :poll="status.poll" :status-id="status.id" /> <poll :base-poll="status.poll" />
</div> </div>
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body"> <div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">

View file

@ -3,7 +3,7 @@
<div v-if="user" class="user-profile panel panel-default"> <div v-if="user" class="user-profile panel panel-default">
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/> <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher"> <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"> <div class="timeline">
<template v-for="statusId in user.pinnedStatuseIds"> <template v-for="statusId in user.pinnedStatuseIds">
<Conversation <Conversation

View file

@ -19,7 +19,8 @@ const saveImmedeatelyActions = [
'setHighlight', 'setHighlight',
'setOption', 'setOption',
'setClientData', 'setClientData',
'setToken' 'setToken',
'clearToken'
] ]
const defaultStorage = (() => { const defaultStorage = (() => {

View file

@ -14,6 +14,7 @@ import authFlowModule from './modules/auth_flow.js'
import mediaViewerModule from './modules/media_viewer.js' import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js' import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js' import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -72,7 +73,8 @@ const persistedStateOptions = {
authFlow: authFlowModule, authFlow: authFlowModule,
mediaViewer: mediaViewerModule, mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule, oauthTokens: oauthTokensModule,
reports: reportsModule reports: reportsModule,
polls: pollsModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -21,7 +21,7 @@ const chat = {
}, },
actions: { actions: {
disconnectFromChat (store) { disconnectFromChat (store) {
store.state.socket.disconnect() store.state.socket && store.state.socket.disconnect()
}, },
initializeChat (store, socket) { initializeChat (store, socket) {
const channel = socket.channel('chat:public') const channel = socket.channel('chat:public')

View file

@ -1,3 +1,5 @@
import { delete as del } from 'vue'
const oauth = { const oauth = {
state: { state: {
clientId: false, clientId: false,
@ -22,6 +24,12 @@ const oauth = {
}, },
setToken (state, token) { setToken (state, token) {
state.userToken = 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: { getters: {

70
src/modules/polls.js Normal file
View 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

View file

@ -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 // Sanity check
if (!isArray(statuses)) { if (!isArray(statuses)) {
return false return false
@ -543,7 +544,7 @@ const statuses = {
}, },
fetchPinnedStatuses ({ rootState, dispatch }, userId) { fetchPinnedStatuses ({ rootState, dispatch }, userId) {
rootState.api.backendInteractor.fetchPinnedStatuses(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) { pinStatus ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.pinOwnStatus(statusId) return rootState.api.backendInteractor.pinOwnStatus(statusId)
@ -582,18 +583,6 @@ const statuses = {
]).then(([favoritedByUsers, rebloggedByUsers]) => ]).then(([favoritedByUsers, rebloggedByUsers]) =>
commit('addFavsAndRepeats', { id, 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 mutations

View file

@ -399,7 +399,7 @@ const users = {
logout (store) { logout (store) {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.dispatch('disconnectFromChat') store.dispatch('disconnectFromChat')
store.commit('setToken', false) store.commit('clearToken')
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications') store.dispatch('stopFetching', 'notifications')

View file

@ -6,7 +6,8 @@ const search = ({query, store}) => {
store, store,
url: '/api/v1/accounts/search', url: '/api/v1/accounts/search',
params: { params: {
q: query q: query,
resolve: true
} }
}) })
.then((data) => data.json()) .then((data) => data.json())