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: 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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