Merge remote-tracking branch 'upstream/develop' into feature/theming2

* upstream/develop: (60 commits)
  whoops
  whoops
  DM timeline: stream new statuses
  update-japanese-translation
  Add actual user search.
  incorporate most translation changes from MR 368
  update french translation
  Always show dm panel.
  Add direct message tab.
  api service url
  remove deploy stage
  remove deploy stage
  updated and completed German translation
  On logout switch to public timeline.
  minor modification of Chinese translation
  update Chinese translation
  Add Chinese language
  Fix posting.
  Put oauth text into description.
  Display OAuth login on login form button.
  ...
This commit is contained in:
Henry Jameson 2018-11-26 05:21:58 +03:00
commit a806d43f05
56 changed files with 2963 additions and 383 deletions

View file

@ -0,0 +1,14 @@
import Timeline from '../timeline/timeline.vue'
const DMs = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.dms
}
},
components: {
Timeline
}
}
export default DMs

View file

@ -0,0 +1,5 @@
<template>
<Timeline :title="$t('nav.dms')" v-bind:timeline="timeline" v-bind:timeline-name="'dms'"/>
</template>
<script src="./dm_timeline.js"></script>

View file

@ -2,6 +2,9 @@ const FavoriteButton = {
props: ['status', 'loggedIn'],
data () {
return {
hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
? this.$store.state.instance.hidePostStats
: this.$store.state.config.hidePostStats,
animated: false
}
},

View file

@ -1,11 +1,11 @@
<template>
<div v-if="loggedIn">
<i :class='classes' class='favorite-button fav-active' @click.prevent='favorite()'/>
<span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
<span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
</div>
<div v-else>
<i :class='classes' class='favorite-button'/>
<span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
<span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
</div>
</template>

View file

@ -1,22 +1,40 @@
import oauthApi from '../../services/new_api/oauth.js'
const LoginForm = {
data: () => ({
user: {},
authError: false
}),
computed: {
loginMethod () { return this.$store.state.instance.loginMethod },
loggingIn () { return this.$store.state.users.loggingIn },
registrationOpen () { return this.$store.state.instance.registrationOpen }
},
methods: {
oAuthLogin () {
oauthApi.login({
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server,
commit: this.$store.commit
})
},
submit () {
this.$store.dispatch('loginUser', this.user).then(
() => {},
(error) => {
this.authError = error
this.user.username = ''
this.user.password = ''
}
)
const data = {
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server
}
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
}
}
}

View file

@ -5,7 +5,7 @@
{{$t('login.login')}}
</div>
<div class="panel-body">
<form v-on:submit.prevent='submit(user)' class='login-form'>
<form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
<div class='form-group'>
<label for='username'>{{$t('login.username')}}</label>
<input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
@ -20,8 +20,17 @@
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
</div>
</div>
<div v-if="authError" class='form-group'>
<div class='alert error'>{{authError}}</div>
</form>
<form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
<div class="form-group">
<p>{{$t('login.description')}}</p>
</div>
<div class='form-group'>
<div class='login-bottom'>
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
</div>
</div>
</form>
</div>

View file

@ -12,6 +12,11 @@
{{ $t("nav.mentions") }}
</router-link>
</li>
<li v-if='currentUser'>
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
<li v-if='currentUser && currentUser.locked'>
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}

View file

@ -0,0 +1,20 @@
import oauth from '../../services/new_api/oauth.js'
const oac = {
props: ['code'],
mounted () {
if (this.code) {
oauth.getToken({
app: this.$store.state.oauth,
instance: this.$store.state.instance.server,
code: this.code
}).then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
}
}
}
export default oac

View file

@ -0,0 +1,5 @@
<template>
<h1>...</h1>
</template>
<script src="./oauth_callback.js"></script>

View file

@ -24,7 +24,7 @@ const PostStatusForm = {
'replyTo',
'repliedUser',
'attentions',
'messageScope',
'copyMessageScope',
'subject'
],
components: {
@ -46,6 +46,10 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
const scope = (this.copyMessageScope && this.$store.state.config.copyScope || this.copyMessageScope === 'direct')
? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope
return {
dropFiles: [],
submitDisabled: false,
@ -53,12 +57,12 @@ const PostStatusForm = {
posting: false,
highlighted: 0,
newStatus: {
spoilerText: this.subject,
spoilerText: this.subject || '',
status: statusText,
contentType: 'text/plain',
nsfw: false,
files: [],
visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
visibility: scope
},
caret: 0
}
@ -128,6 +132,9 @@ const PostStatusForm = {
statusLength () {
return this.newStatus.status.length
},
spoilerTextLength () {
return this.newStatus.spoilerText.length
},
statusLengthLimit () {
return this.$store.state.instance.textlimit
},
@ -135,10 +142,10 @@ const PostStatusForm = {
return this.statusLengthLimit > 0
},
charactersLeft () {
return this.statusLengthLimit - this.statusLength
return this.statusLengthLimit - (this.statusLength + this.spoilerTextLength)
},
isOverLengthLimit () {
return this.hasStatusLengthLimit && (this.statusLength > this.statusLengthLimit)
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
},
scopeOptionsEnabled () {
return this.$store.state.instance.scopeOptionsEnabled
@ -223,6 +230,7 @@ const PostStatusForm = {
if (!data.error) {
this.newStatus = {
status: '',
spoilerText: '',
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType

View file

@ -1,3 +1,5 @@
import oauthApi from '../../services/new_api/oauth.js'
const registration = {
data: () => ({
user: {},
@ -25,9 +27,23 @@ const registration = {
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
this.$store.dispatch('loginUser', this.user)
this.$router.push('/main/all')
this.registering = false
const data = {
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server
}
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
} else {
this.registering = false
response.json().then((data) => {

View file

@ -2,6 +2,9 @@ const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
data () {
return {
hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
? this.$store.state.instance.hidePostStats
: this.$store.state.config.hidePostStats,
animated: false
}
},

View file

@ -2,7 +2,7 @@
<div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'">
<i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
<span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
</template>
<template v-else>
<i :class='classes' class='icon-lock' :title="$t('timeline.no_retweet_hint')"></i>
@ -10,7 +10,7 @@
</div>
<div v-else-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
<span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
</div>
</template>

View file

@ -13,6 +13,14 @@ const settings = {
hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
hideNsfwLocal: user.hideNsfw,
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
? instance.hidePostStats
: user.hidePostStats,
hidePostStatsDefault: this.$t('settings.values.' + instance.hidePostStats),
hideUserStatsLocal: typeof user.hideUserStats === 'undefined'
? instance.hideUserStats
: user.hideUserStats,
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
notificationVisibilityLocal: user.notificationVisibility,
replyVisibilityLocal: user.replyVisibility,
loopVideoLocal: user.loopVideo,
@ -26,6 +34,12 @@ const settings = {
? instance.collapseMessageWithSubject
: user.collapseMessageWithSubject,
collapseMessageWithSubjectDefault: this.$t('settings.values.' + instance.collapseMessageWithSubject),
subjectLineBehaviorLocal: typeof user.subjectLineBehavior === 'undefined'
? instance.subjectLineBehavior
: user.subjectLineBehavior,
subjectLineBehaviorDefault: instance.subjectLineBehavior,
scopeCopyLocal: user.scopeCopy,
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
stopGifs: user.stopGifs,
loopSilentAvailable:
// Firefox
@ -56,6 +70,12 @@ const settings = {
hideAttachmentsInConvLocal (value) {
this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
},
hidePostStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hidePostStats', value })
},
hideUserStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
},
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
@ -99,6 +119,12 @@ const settings = {
collapseMessageWithSubjectLocal (value) {
this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
},
scopeCopyLocal (value) {
this.$store.dispatch('setOption', { name: 'scopeCopy', value })
},
subjectLineBehaviorLocal (value) {
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
},
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
}

View file

@ -54,6 +54,41 @@
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{$t('settings.composing')}}</h2>
<ul class="setting-list">
<li>
<input type="checkbox" id="scopeCopy" v-model="scopeCopyLocal">
<label for="scopeCopy">
{{$t('settings.scope_copy')}} {{$t('settings.instance_default', { value: scopeCopyDefault })}}
</label>
</li>
<li>
<div>
{{$t('settings.subject_line_behavior')}}
<label for="subjectLineBehavior" class="select">
<select id="subjectLineBehavior" v-model="subjectLineBehaviorLocal">
<option value="email">
{{$t('settings.subject_line_email')}}
{{subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : ''}}
</option>
<option value="masto">
{{$t('settings.subject_line_mastodon')}}
{{subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : ''}}
</option>
<option value="noop">
{{$t('settings.subject_line_noop')}}
{{subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : ''}}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{$t('settings.attachments')}}</h2>
<ul class="setting-list">
@ -139,6 +174,18 @@
<i class="icon-down-open"/>
</label>
</div>
<div>
<input type="checkbox" id="hidePostStats" v-model="hidePostStatsLocal">
<label for="hidePostStats">
{{$t('settings.hide_post_stats')}} {{$t('settings.instance_default', { value: hidePostStatsDefault })}}
</label>
</div>
<div>
<input type="checkbox" id="hideUserStats" v-model="hideUserStatsLocal">
<label for="hideUserStats">
{{$t('settings.hide_user_stats')}} {{$t('settings.instance_default', { value: hideUserStatsDefault })}}
</label>
</div>
</div>
<div class="setting-item">
<p>{{$t('settings.filtering_explanation')}}</p>

View file

@ -31,10 +31,17 @@ const Status = {
preview: null,
showPreview: false,
showingTall: false,
expandingSubject: !this.$store.state.config.collapseMessageWithSubject
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject
}
},
computed: {
localCollapseSubjectDefault () {
return typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? this.$store.state.instance.collapseMessageWithSubject
: this.$store.state.config.collapseMessageWithSubject
},
muteWords () {
return this.$store.state.config.muteWords
},
@ -147,13 +154,13 @@ const Status = {
return this.status.attentions.length > 0
},
hideSubjectStatus () {
if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) {
if (this.tallStatus && !this.localCollapseSubjectDefault) {
return false
}
return !this.expandingSubject && this.status.summary
},
hideTallStatus () {
if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
if (this.status.summary && this.localCollapseSubjectDefault) {
return false
}
if (this.showingTall) {
@ -168,16 +175,22 @@ const Status = {
if (!this.status.nsfw) {
return false
}
if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
if (this.status.summary && this.localCollapseSubjectDefault) {
return false
}
return true
},
replySubject () {
if (this.status.summary && !this.status.summary.match(/^re[: ]/i)) {
if (!this.status.summary) return ''
const behavior = this.$store.state.config.subjectLineBehavior
const startsWithRe = this.status.summary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
return this.status.summary
} else if (behavior === 'email') {
return 're: '.concat(this.status.summary)
} else if (behavior === 'noop') {
return ''
}
return this.status.summary
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||

View file

@ -106,7 +106,7 @@
</div>
<div class="container" v-if="replying">
<div class="reply-left"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
</div>
</template>
</div>

View file

@ -3,6 +3,13 @@ import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default {
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
data () {
return {
hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
? this.$store.state.instance.hideUserStats
: this.$store.state.config.hideUserStats
}
},
computed: {
headingStyle () {
const color = this.$store.state.config.customTheme.colors.bg

View file

@ -17,7 +17,7 @@
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
</div>
</div>
@ -91,23 +91,24 @@
</div>
</div>
<div class="panel-body profile-panel-body">
<div class="user-counts" :class="{clickable: switcher}">
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{user.statuses_count}} <br></span>
<span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
</div>
<div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}">
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{user.friends_count}}</span>
<span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
</div>
<div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}">
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{user.followers_count}}</span>
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
</div>
</div>
<p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div>
<p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div>
</div>
</template>

View file

@ -7,25 +7,10 @@ const UserFinder = {
}),
methods: {
findUser (username) {
username = username[0] === '@' ? username.slice(1) : username
this.loading = true
this.$store.state.api.backendInteractor.externalProfile(username)
.then((user) => {
this.loading = false
this.hidden = true
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$router.push({name: 'user-profile', params: {id: user.id}})
} else {
this.error = true
}
})
this.$router.push({ name: 'user-search', query: { query: username } })
},
toggleHidden () {
this.hidden = !this.hidden
},
dismissError () {
this.error = false
}
}
}

View file

@ -1,9 +1,5 @@
<template>
<span class="user-finder-container">
<span class="alert error" v-if="error">
<i class="icon-cancel user-finder-icon" @click="dismissError"/>
{{$t('finder.error_fetching_user')}}
</span>
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden"/></a>
<span v-else>

View file

@ -0,0 +1,33 @@
import UserCard from '../user_card/user_card.vue'
import userSearchApi from '../../services/new_api/user_search.js'
const userSearch = {
components: {
UserCard
},
props: [
'query'
],
data () {
return {
users: []
}
},
mounted () {
this.search(this.query)
},
watch: {
query (newV) {
this.search(newV)
}
},
methods: {
search (query) {
userSearchApi.search({query, store: this.$store})
.then((res) => {
this.users = res
})
}
}
}
export default userSearch

View file

@ -0,0 +1,12 @@
<template>
<div class="user-seach panel panel-default">
<div class="panel-heading">
{{$t('nav.user_search')}}
</div>
<div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
</div>
</div>
</template>
<script src="./user_search.js"></script>