Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop:
  Rename expandCW to collapseMessageWithSubject.
  fix indent
  Add support for configurable CW clickthrough.
  Merge upstream
  fix lint issues
  allow default visibility scope to be configured
  Add validation of the imported theme and the corresponding warning message
  Unify button styles and use min-width
  Add German localization for theme import/export
  Add theme import feature
  Refactor theme settings state initialization
  Add theme export feature
This commit is contained in:
Henry Jameson 2018-08-20 12:10:40 +03:00
commit e246a3beee
16 changed files with 235 additions and 80 deletions

View file

@ -34,11 +34,6 @@
@import '../../_variables.scss';
.login-form {
.btn {
min-height: 28px;
width: 10em;
}
.error {
text-align: center;
}

View file

@ -54,7 +54,7 @@ const PostStatusForm = {
newStatus: {
status: statusText,
files: [],
visibility: this.messageScope || 'public'
visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
},
caret: 0
}

View file

@ -101,35 +101,12 @@
}
}
.post-status-form .visibility-tray {
font-size: 1.2em;
padding: 3px;
cursor: pointer;
.selected {
color: $fallback--lightFg;
color: var(--lightFg, $fallback--lightFg);
}
}
.visibility-notice {
padding: .5em;
border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
}
.post-status-form, .login {
.form-bottom {
display: flex;
padding: 0.5em;
height: 32px;
button {
width: 10em;
}
p {
margin: 0.35em;
padding: 0.35em;

View file

@ -15,6 +15,7 @@ const settings = {
streamingLocal: this.$store.state.config.streaming,
pauseOnUnfocusedLocal: this.$store.state.config.pauseOnUnfocused,
hoverPreviewLocal: this.$store.state.config.hoverPreview,
collapseMessageWithSubjectLocal: this.$store.state.config.collapseMessageWithSubject,
stopGifs: this.$store.state.config.stopGifs,
loopSilentAvailable:
// Firefox
@ -65,6 +66,9 @@ const settings = {
value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value })
},
collapseMessageWithSubjectLocal (value) {
this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
},
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
}

View file

@ -16,6 +16,10 @@
<div class="setting-item">
<h2>{{$t('nav.timeline')}}</h2>
<ul class="setting-list">
<li>
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</label>
</li>
<li>
<input type="checkbox" id="streaming" v-model="streamingLocal">
<label for="streaming">{{$t('settings.streaming')}}</label>
@ -113,8 +117,6 @@
.btn {
margin-top: 1em;
min-height: 28px;
width: 10em;
}
}
.setting-list {

View file

@ -22,15 +22,18 @@ const Status = {
'noHeading',
'inlineExpanded'
],
data: () => ({
replying: false,
expanded: false,
unmuted: false,
userExpanded: false,
preview: null,
showPreview: false,
showingTall: false
}),
data () {
return {
replying: false,
expanded: false,
unmuted: false,
userExpanded: false,
preview: null,
showPreview: false,
showingTall: false,
expandingSubject: !this.$store.state.config.collapseMessageWithSubject
}
},
computed: {
muteWords () {
return this.$store.state.config.muteWords
@ -98,12 +101,27 @@ const Status = {
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
hideSubjectStatus () {
if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) {
return false
}
return !this.expandingSubject && this.status.summary
},
hideTallStatus () {
if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
return false
}
if (this.showingTall) {
return false
}
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
return this.tallStatus
},
showingMore () {
return this.showingTall || (this.status.summary && this.expandingSubject)
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
@ -163,8 +181,16 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
toggleShowTall () {
this.showingTall = !this.showingTall
toggleShowMore () {
if (this.showingTall) {
this.showingTall = false
} else if (this.expandingSubject) {
this.expandingSubject = false
} else if (this.hideTallStatus) {
this.showingTall = true
} else if (this.hideSubjectStatus) {
this.expandingSubject = true
}
},
replyEnter (id, event) {
this.showPreview = true

View file

@ -76,9 +76,11 @@
</div>
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowTall">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
<a v-if="showingTall" href="#" class="tall-status-unhider" @click.prevent="toggleShowTall">Show less</a>
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
<div v-if='status.attachments' class='attachments media-body'>
@ -310,7 +312,7 @@
}
}
.tall-status-unhider {
.status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}

View file

@ -5,6 +5,7 @@ export default {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
invalidThemeImported: false,
bgColorLocal: '',
btnColorLocal: '',
textColorLocal: '',
@ -32,25 +33,61 @@ export default {
})
},
mounted () {
this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors.bg)
this.btnColorLocal = rgbstr2hex(this.$store.state.config.colors.btn)
this.textColorLocal = rgbstr2hex(this.$store.state.config.colors.fg)
this.linkColorLocal = rgbstr2hex(this.$store.state.config.colors.link)
this.redColorLocal = rgbstr2hex(this.$store.state.config.colors.cRed)
this.blueColorLocal = rgbstr2hex(this.$store.state.config.colors.cBlue)
this.greenColorLocal = rgbstr2hex(this.$store.state.config.colors.cGreen)
this.orangeColorLocal = rgbstr2hex(this.$store.state.config.colors.cOrange)
this.btnRadiusLocal = this.$store.state.config.radii.btnRadius || 4
this.inputRadiusLocal = this.$store.state.config.radii.inputRadius || 4
this.panelRadiusLocal = this.$store.state.config.radii.panelRadius || 10
this.avatarRadiusLocal = this.$store.state.config.radii.avatarRadius || 5
this.avatarAltRadiusLocal = this.$store.state.config.radii.avatarAltRadius || 50
this.tooltipRadiusLocal = this.$store.state.config.radii.tooltipRadius || 2
this.attachmentRadiusLocal = this.$store.state.config.radii.attachmentRadius || 5
this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
},
methods: {
exportCurrentTheme () {
const stringified = JSON.stringify({
// To separate from other random JSON files and possible future theme formats
_pleroma_theme_version: 1,
colors: this.$store.state.config.colors,
radii: this.$store.state.config.radii
}, null, 2) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
e.setAttribute('download', 'pleroma_theme.json')
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.style.display = 'none'
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
},
importTheme () {
this.invalidThemeImported = false
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
filePicker.setAttribute('accept', '.json')
filePicker.addEventListener('change', event => {
if (event.target.files[0]) {
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({target}) => {
try {
const parsed = JSON.parse(target.result)
if (parsed._pleroma_theme_version === 1) {
this.normalizeLocalState(parsed.colors, parsed.radii)
} else {
// A theme from the future, spooky
this.invalidThemeImported = true
}
} catch (e) {
// This will happen both if there is a JSON syntax error or the theme is missing components
this.invalidThemeImported = true
}
}
reader.readAsText(event.target.files[0])
}
})
document.body.appendChild(filePicker)
filePicker.click()
document.body.removeChild(filePicker)
},
setCustomTheme () {
if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
// reset to picked themes
@ -95,6 +132,26 @@ export default {
attachmentRadius: this.attachmentRadiusLocal
}})
}
},
normalizeLocalState (colors, radii) {
this.bgColorLocal = rgbstr2hex(colors.bg)
this.btnColorLocal = rgbstr2hex(colors.btn)
this.textColorLocal = rgbstr2hex(colors.fg)
this.linkColorLocal = rgbstr2hex(colors.link)
this.redColorLocal = rgbstr2hex(colors.cRed)
this.blueColorLocal = rgbstr2hex(colors.cBlue)
this.greenColorLocal = rgbstr2hex(colors.cGreen)
this.orangeColorLocal = rgbstr2hex(colors.cOrange)
this.btnRadiusLocal = radii.btnRadius || 4
this.inputRadiusLocal = radii.inputRadius || 4
this.panelRadiusLocal = radii.panelRadius || 10
this.avatarRadiusLocal = radii.avatarRadius || 5
this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
this.tooltipRadiusLocal = radii.tooltipRadius || 2
this.attachmentRadiusLocal = radii.attachmentRadius || 5
}
},
watch: {

View file

@ -11,6 +11,11 @@
<i class="icon-down-open"/>
</label>
</div>
<div>
<button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
<button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
<p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
</div>
<div class="color-container">
<p>{{$t('settings.theme_help')}}</p>
<div class="color-item">
@ -134,6 +139,11 @@
margin-right: 1em;
}
.import-warning {
color: $fallback--cRed;
color: var(--cRed, $fallback--cRed);
}
.radius-container,
.color-container {
display: flex;

View file

@ -6,6 +6,7 @@ const UserSettings = {
newname: this.$store.state.users.currentUser.name,
newbio: this.$store.state.users.currentUser.description,
newlocked: this.$store.state.users.currentUser.locked,
newdefaultScope: this.$store.state.users.currentUser.default_scope,
followList: null,
followImportError: false,
followsImported: false,
@ -17,7 +18,8 @@ const UserSettings = {
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
changePasswordError: false
changePasswordError: false,
activeTab: 'profile'
}
},
components: {
@ -29,6 +31,17 @@ const UserSettings = {
},
pleromaBackend () {
return this.$store.state.config.pleromaBackend
},
scopeOptionsEnabled () {
return this.$store.state.config.scopeOptionsEnabled
},
vis () {
return {
public: { selected: this.newdefaultScope === 'public' },
unlisted: { selected: this.newdefaultScope === 'unlisted' },
private: { selected: this.newdefaultScope === 'private' },
direct: { selected: this.newdefaultScope === 'direct' }
}
}
},
methods: {
@ -36,12 +49,18 @@ const UserSettings = {
const name = this.newname
const description = this.newbio
const locked = this.newlocked
this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked}}).then((user) => {
/* eslint-disable camelcase */
const default_scope = this.newdefaultScope
this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
/* eslint-enable camelcase */
},
changeVis (visibility) {
this.newdefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
@ -217,6 +236,9 @@ const UserSettings = {
this.changePasswordError = res.error
}
})
},
activateTab (tabName) {
this.activeTab = tabName
}
}
}

View file

@ -4,19 +4,33 @@
{{$t('settings.user_settings')}}
</div>
<div class="panel-body profile-edit">
<div class="setting-item">
<div class="tab-switcher">
<button class="btn btn-default" @click="activateTab('profile')">{{$t('settings.profile_tab')}}</button>
<button class="btn btn-default" @click="activateTab('security')">{{$t('settings.security_tab')}}</button>
<button class="btn btn-default" @click="activateTab('data_import_export')" v-if="pleromaBackend">{{$t('settings.data_import_export_tab')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<input class='name-changer' id='username' v-model="newname"></input>
<p>{{$t('settings.bio')}}</p>
<textarea class="bio" v-model="newbio"></textarea>
<div class="setting-item">
<p>
<input type="checkbox" v-model="newlocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p>
<div v-if="scopeOptionsEnabled">
<label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
</div>
</div>
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.avatar')}}</h2>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
@ -29,7 +43,7 @@
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner"></img>
@ -42,7 +56,7 @@
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
@ -53,7 +67,7 @@
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
@ -72,7 +86,7 @@
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item" v-if="pleromaBackend">
<div class="setting-item" v-if="pleromaBackend && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form v-model="followImportForm">
@ -89,15 +103,15 @@
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
<div class="setting-item" v-if="enableFollowsExport">
<div class="setting-item" v-if="enableFollowsExport && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export')}}</h2>
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
</div>
<div class="setting-item" v-else>
<div class="setting-item" v-else-if="activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
<hr>
<div class="setting-item">
<div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
@ -137,4 +151,13 @@
margin: 0.25em;
}
}
.tab-switcher {
margin: 7px 7px;
display: inline-block;
button {
height: 30px;
}
}
</style>