Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85629927
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
27 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/user_autosuggest/user_autosuggest.js b/src/components/user_autosuggest/user_autosuggest.js
index 12a6964a02..d4efe912a7 100644
--- a/src/components/user_autosuggest/user_autosuggest.js
+++ b/src/components/user_autosuggest/user_autosuggest.js
@@ -1,57 +1,52 @@
-import BlockCard from '../block_card/block_card.vue'
-
const debounceMilliseconds = 500
export default {
props: {
query: { // function to query results and return a promise
type: Function,
required: true
},
filter: { // function to filter results in real time
type: Function
},
placeholder: {
type: String,
default: 'Search...'
}
},
- components: {
- BlockCard
- },
data () {
return {
term: '',
timeout: null,
results: [],
resultsVisible: false
}
},
computed: {
filtered () {
return this.filter ? this.filter(this.results) : this.results
}
},
watch: {
term (val) {
this.fetchResults(val)
}
},
methods: {
fetchResults (term) {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.results = []
if (term) {
this.query(term).then((results) => { this.results = results })
}
}, debounceMilliseconds)
},
onInputClick () {
this.resultsVisible = true
},
onClickOutside () {
this.resultsVisible = false
}
}
}
diff --git a/src/components/user_autosuggest/user_autosuggest.vue b/src/components/user_autosuggest/user_autosuggest.vue
index db3e1bdfc0..6eaecbaa6f 100644
--- a/src/components/user_autosuggest/user_autosuggest.vue
+++ b/src/components/user_autosuggest/user_autosuggest.vue
@@ -1,45 +1,46 @@
<template>
<div class="user-autosuggest" v-click-outside="onClickOutside">
<input v-model="term" :placeholder="placeholder" @click="onInputClick" class="user-autosuggest-input" />
<div class="user-autosuggest-results" v-if="resultsVisible && filtered.length > 0">
- <BlockCard v-for="userId in filtered" :key="userId" :userId="userId"/>
+ <slot v-for="item in filtered" :item="item">
+ </slot>
</div>
</div>
</template>
<script src="./user_autosuggest.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-autosuggest {
position: relative;
&-input {
display: block;
width: 100%;
}
&-results {
position: absolute;
left: 0;
top: 100%;
right: 0;
max-height: 400px;
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
overflow-y: auto;
z-index: 1;
}
}
</style>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 259e341841..2cae2d00b9 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,359 +1,360 @@
import { compose } from 'vue-compose'
import unescape from 'lodash/unescape'
import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
import UserAutoSuggest from '../user_autosuggest/user_autosuggest.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import withList from '../../hocs/with_list/with_list'
import userSearchApi from '../../services/new_api/user_search.js'
const BlockList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(BlockCard)
const MuteList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(MuteCard)
const UserSettings = {
data () {
return {
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope,
hideFollows: this.$store.state.users.currentUser.hide_follows,
hideFollowers: this.$store.state.users.currentUser.hide_followers,
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
followList: null,
followImportError: false,
followsImported: false,
enableFollowsExport: true,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
bannerPreview: null,
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
changePasswordError: false,
activeTab: 'profile'
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
StyleSwitcher,
ScopeSelector,
TabSwitcher,
ImageCropper,
BlockList,
MuteList,
EmojiInput,
- UserAutoSuggest
+ UserAutoSuggest,
+ BlockCard
},
computed: {
user () {
return this.$store.state.users.currentUser
},
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
minimalScopesMode () {
return this.$store.state.instance.minimalScopesMode
},
vis () {
return {
public: { selected: this.newDefaultScope === 'public' },
unlisted: { selected: this.newDefaultScope === 'unlisted' },
private: { selected: this.newDefaultScope === 'private' },
direct: { selected: this.newDefaultScope === 'direct' }
}
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
}
},
methods: {
updateProfile () {
const name = this.newName
const description = this.newBio
const locked = this.newLocked
// Backend notation.
/* eslint-disable camelcase */
const default_scope = this.newDefaultScope
const no_rich_text = this.newNoRichText
const hide_follows = this.hideFollows
const hide_followers = this.hideFollowers
const show_role = this.showRole
/* eslint-enable camelcase */
this.$store.state.api.backendInteractor
.updateProfile({
params: {
name,
description,
locked,
// Backend notation.
/* eslint-disable camelcase */
default_scope,
no_rich_text,
hide_follows,
hide_followers,
show_role
/* eslint-enable camelcase */
}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
},
changeVis (visibility) {
this.newDefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
if (!file) { return }
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
return
}
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({target}) => {
const img = target.result
this[slot + 'Preview'] = img
}
reader.readAsDataURL(file)
},
submitAvatar (cropper, file) {
let img
if (cropper) {
img = cropper.getCroppedCanvas().toDataURL(file.type)
} else {
img = file
}
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
} else {
throw new Error(this.$t('upload.error.base') + user.error)
}
})
},
clearUploadError (slot) {
this[slot + 'UploadError'] = null
},
submitBanner () {
if (!this.bannerPreview) { return }
let banner = this.bannerPreview
// eslint-disable-next-line no-undef
let imginfo = new Image()
/* eslint-disable camelcase */
let offset_top, offset_left, width, height
imginfo.src = banner
width = imginfo.width
height = imginfo.height
offset_top = 0
offset_left = 0
this.bannerUploading = true
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
if (!data.error) {
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
clone.cover_photo = data.url
this.$store.commit('addNewUsers', [clone])
this.$store.commit('setCurrentUser', clone)
this.bannerPreview = null
} else {
this.bannerUploadError = this.$t('upload.error.base') + data.error
}
this.bannerUploading = false
})
/* eslint-enable camelcase */
},
submitBg () {
if (!this.backgroundPreview) { return }
let img = this.backgroundPreview
// eslint-disable-next-line no-undef
let imginfo = new Image()
let cropX, cropY, cropW, cropH
imginfo.src = img
cropX = 0
cropY = 0
cropW = imginfo.width
cropH = imginfo.width
this.backgroundUploading = true
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
if (!data.error) {
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
clone.background_image = data.url
this.$store.commit('addNewUsers', [clone])
this.$store.commit('setCurrentUser', clone)
this.backgroundPreview = null
} else {
this.backgroundUploadError = this.$t('upload.error.base') + data.error
}
this.backgroundUploading = false
})
},
importFollows () {
this.followListUploading = true
const followList = this.followList
this.$store.state.api.backendInteractor.followImport({params: followList})
.then((status) => {
if (status) {
this.followsImported = true
} else {
this.followImportError = true
}
this.followListUploading = false
})
},
/* This function takes an Array of Users
* and outputs a file with all the addresses for the user to download
*/
exportPeople (users, filename) {
// Get all the friends addresses
var UserAddresses = users.map(function (user) {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
user.screen_name += '@' + location.hostname
}
return user.screen_name
}).join('\n')
// Make the user download the file
var fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(UserAddresses))
fileToDownload.setAttribute('download', filename)
fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload)
fileToDownload.click()
document.body.removeChild(fileToDownload)
},
exportFollows () {
this.enableFollowsExport = false
this.$store.state.api.backendInteractor
.exportFriends({
id: this.$store.state.users.currentUser.id
})
.then((friendList) => {
this.exportPeople(friendList, 'friends.csv')
setTimeout(() => { this.enableFollowsExport = true }, 2000)
})
},
followListChange () {
// eslint-disable-next-line no-undef
let formData = new FormData()
formData.append('list', this.$refs.followlist.files[0])
this.followList = formData
},
dismissImported () {
this.followsImported = false
this.followImportError = false
},
confirmDelete () {
this.deletingAccount = true
},
deleteAccount () {
this.$store.state.api.backendInteractor.deleteAccount({password: this.deleteAccountConfirmPasswordInput})
.then((res) => {
if (res.status === 'success') {
this.$store.dispatch('logout')
this.$router.push({name: 'root'})
} else {
this.deleteAccountError = res.error
}
})
},
changePassword () {
const params = {
password: this.changePasswordInputs[0],
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
this.changePasswordError = false
this.logout()
} else {
this.changedPassword = false
this.changePasswordError = res.error
}
})
},
activateTab (tabName) {
this.activeTab = tabName
},
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
},
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
})
},
queryUserIds (query) {
return userSearchApi.search({query, store: this.$store})
.then((users) => {
this.$store.dispatch('addNewUsers', users)
return map(users, 'id')
})
}
}
}
export default UserSettings
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 3dde5ae11d..69d3dc538f 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -1,273 +1,275 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
{{$t('settings.user_settings')}}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
{{ $t('settings.saving_err') }}
</div>
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
<tab-switcher>
<div :label="$t('settings.profile_tab')">
<div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<EmojiInput
type="text"
v-model="newName"
id="username"
classname="name-changer"
/>
<p>{{$t('settings.bio')}}</p>
<EmojiInput
type="textarea"
v-model="newBio"
classname="bio"
/>
<p>
<input type="checkbox" v-model="newLocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p>
<div>
<label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray">
<scope-selector
:showAll="true"
:userDefault="newDefaultScope"
:onScopeChange="changeVis"/>
</div>
</div>
<p>
<input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="hideFollows" id="account-hide-follows">
<label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
<label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="showRole" id="account-show-role">
<label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
<label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
</p>
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="current-avatar" />
<p>{{$t('settings.set_new_avatar')}}</p>
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner" />
<p>{{$t('settings.set_new_profile_banner')}}</p>
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview" />
<div>
<input type="file" @change="uploadFile('banner', $event)" />
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
<div class='alert error' v-if="bannerUploadError">
Error: {{ bannerUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('banner')"></i>
</div>
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview" />
<div>
<input type="file" @change="uploadFile('background', $event)" />
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
<div class='alert error' v-if="backgroundUploadError">
Error: {{ backgroundUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('background')"></i>
</div>
</div>
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
<input type="password" v-model="changePasswordInputs[0]">
</div>
<div>
<p>{{$t('settings.new_password')}}</p>
<input type="password" v-model="changePasswordInputs[1]">
</div>
<div>
<p>{{$t('settings.confirm_new_password')}}</p>
<input type="password" v-model="changePasswordInputs[2]">
</div>
<button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
<p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{$t('settings.app_name')}}</th>
<th>{{$t('settings.valid_until')}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.appName}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">
{{$t('settings.revoke_token')}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
<p>{{$t('settings.delete_account_instructions')}}</p>
<p>{{$t('login.password')}}</p>
<input type="password" v-model="deleteAccountConfirmPasswordInput">
<button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
</div>
<p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
<p v-if="deleteAccountError">{{deleteAccountError}}</p>
<button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
</div>
</div>
<div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
<div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form>
<input type="file" ref="followlist" v-on:change="followListChange" />
</form>
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
<div v-if="followsImported">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follows_imported')}}</p>
</div>
<div v-else-if="followImportError">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
<div class="setting-item" v-if="enableFollowsExport">
<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>
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
</div>
<div :label="$t('settings.blocks_tab')">
<div class="profile-edit-usersearch-wrapper">
- <UserAutoSuggest :filter="filterUnblockedUsers" :query="queryUserIds" placeholder="Search whom you want to block" />
+ <UserAutoSuggest :filter="filterUnblockedUsers" :query="queryUserIds" placeholder="Search whom you want to block">
+ <BlockCard slot-scope="row" :userId="row.item"/>
+ </UserAutoSuggest>
</div>
<block-list :refresh="true">
<template slot="empty">{{$t('settings.no_blocks')}}</template>
</block-list>
</div>
<div :label="$t('settings.mutes_tab')">
<mute-list :refresh="true">
<template slot="empty">{{$t('settings.no_mutes')}}</template>
</mute-list>
</div>
</tab-switcher>
</div>
</div>
</template>
<script src="./user_settings.js">
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
}
input[type=file] {
padding: 5px;
height: auto;
}
.banner {
max-width: 100%;
}
.uploading {
font-size: 1.5em;
margin: 0.25em;
}
.name-changer {
width: 100%;
}
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
&-usersearch-wrapper {
padding: 1em;
}
}
</style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 9, 7:20 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724323
Default Alt Text
(27 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment