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

@ -63,6 +63,8 @@ button{
box-shadow: 0px 0px 2px black;
font-size: 14px;
font-family: sans-serif;
min-width: 10em;
min-height: 2em;
&:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
@ -441,3 +443,23 @@ nav {
text-align: right;
padding-right: 20px;
}
.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);
}

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>

View file

@ -48,6 +48,9 @@ const de = {
settings: 'Einstellungen',
theme: 'Farbschema',
presets: 'Voreinstellungen',
export_theme: 'Aktuelles Theme exportieren',
import_theme: 'Gespeichertes Theme laden',
invalid_theme_imported: 'Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.',
theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen',
radii_help: 'Kantenrundung (in Pixel) der Oberfläche anpassen',
background: 'Hintergrund',
@ -288,7 +291,10 @@ const en = {
settings: 'Settings',
theme: 'Theme',
presets: 'Presets',
export_theme: 'Export current theme',
import_theme: 'Load saved theme',
theme_help: 'Use hex color codes (#rrggbb) to customize your color theme.',
invalid_theme_imported: 'The selected file is not a supported Pleroma theme. No changes to your theme were made.',
radii_help: 'Set up interface edge rounding (in pixels)',
background: 'Background',
foreground: 'Foreground',
@ -311,6 +317,7 @@ const en = {
hide_attachments_in_tl: 'Hide attachments in timeline',
hide_attachments_in_convo: 'Hide attachments in conversations',
nsfw_clickthrough: 'Enable clickthrough NSFW attachment hiding',
collapse_subject: 'Collapse posts with subjects',
stop_gifs: 'Play-on-hover GIFs',
autoload: 'Enable automatic loading when scrolled to the bottom',
streaming: 'Enable automatic streaming of new posts when scrolled to the top',
@ -336,7 +343,11 @@ const en = {
changed_password: 'Password changed successfully!',
change_password_error: 'There was an issue changing your password.',
lock_account_description: 'Restrict your account to approved followers only',
limited_availability: 'Unavailable in your browser'
limited_availability: 'Unavailable in your browser',
default_vis: 'Default visibility scope',
profile_tab: 'Profile',
security_tab: 'Security',
data_import_export_tab: 'Data Import / Export'
},
notifications: {
notifications: 'Notifications',

View file

@ -45,6 +45,7 @@ Vue.use(VueChatScroll)
const persistedStateOptions = {
paths: [
'config.collapseMessageWithSubject',
'config.hideAttachments',
'config.hideAttachmentsInConv',
'config.hideNsfw',
@ -96,7 +97,7 @@ window.fetch('/api/statusnet/config.json')
window.fetch('/static/config.json')
.then((res) => res.json())
.then((data) => {
const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled} = data
const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled, collapseMessageWithSubject} = data
store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo })
@ -105,6 +106,7 @@ window.fetch('/static/config.json')
store.dispatch('setOption', { name: 'whoToFollowLink', value: whoToFollowLink })
store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
store.dispatch('setOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
if (data['chatDisabled']) {
store.dispatch('disableChat')
}

View file

@ -4,6 +4,7 @@ import StyleSetter from '../services/style_setter/style_setter.js'
const defaultState = {
name: 'Pleroma FE',
colors: {},
collapseMessageWithSubject: false,
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,

View file

@ -13,5 +13,6 @@
"whoToFollowLinkDummy2": "https://followlink.osa-p.net/recommend.html",
"showInstanceSpecificPanel": true,
"scopeOptionsEnabled": true,
"registrationOpen": true
"registrationOpen": true,
"collapseMessageWithSubject": false
}