Merge remote-tracking branch 'upstream/develop' into async_follow
* upstream/develop: (45 commits) fix chrome Prevent html-minifier to remove placeholder comment in index.html template Add placeholder to insert server generated metatags. Related to #430 added condition to check for logined user fix gradients and minor artifacts keep track of new instance options fix old MR oof get rid of slots fix timeago font added hide_network option, fixed properties naming Fix fetching new users, add storing local users in usersObjects with their screen_name as well as id, so that they could be fetched zero-state with screen-name link. improve notification subscription Refactor arrays to individual options Reset enableFollowsExport to true after 2 sec when an export file is available to download Write a unit test for fileSizeFormatService add checkbox to disable web push I am dumb Handle errors from server Moved upload errors in user_settings to an array. Moved upload error strings to its separate section in i18n ...
This commit is contained in:
commit
d7973b0b80
40 changed files with 569 additions and 119 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ test/unit/coverage
|
||||||
test/e2e/reports
|
test/e2e/reports
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
.idea/
|
.idea/
|
||||||
|
config/local.json
|
||||||
|
|
|
@ -29,6 +29,15 @@ npm run build
|
||||||
npm run unit
|
npm run unit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# For Contributors:
|
||||||
|
|
||||||
|
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
||||||
|
|
||||||
|
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||||
|
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||||
|
|
||||||
|
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
|
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
|
||||||
|
|
|
@ -2,6 +2,7 @@ var path = require('path')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
|
@ -91,5 +92,10 @@ module.exports = {
|
||||||
browsers: ['last 2 versions']
|
browsers: ['last 2 versions']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
plugins: [
|
||||||
|
new ServiceWorkerWebpackPlugin({
|
||||||
|
entry: path.join(__dirname, '..', 'src/sw.js')
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@ module.exports = merge(baseWebpackConfig, {
|
||||||
devtool: '#eval-source-map',
|
devtool: '#eval-source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env
|
'process.env': config.dev.env,
|
||||||
|
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||||
|
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
|
|
|
@ -10,6 +10,11 @@ var env = process.env.NODE_ENV === 'testing'
|
||||||
? require('../config/test.env')
|
? require('../config/test.env')
|
||||||
: config.build.env
|
: config.build.env
|
||||||
|
|
||||||
|
let commitHash = require('child_process')
|
||||||
|
.execSync('git rev-parse --short HEAD')
|
||||||
|
.toString();
|
||||||
|
console.log(commitHash)
|
||||||
|
|
||||||
var webpackConfig = merge(baseWebpackConfig, {
|
var webpackConfig = merge(baseWebpackConfig, {
|
||||||
module: {
|
module: {
|
||||||
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
|
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
|
||||||
|
@ -29,7 +34,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
plugins: [
|
plugins: [
|
||||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': env
|
'process.env': env,
|
||||||
|
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||||
|
'DEV_OVERRIDES': JSON.stringify(undefined)
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
compress: {
|
compress: {
|
||||||
|
@ -51,7 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
minify: {
|
minify: {
|
||||||
removeComments: true,
|
removeComments: true,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
removeAttributeQuotes: true
|
removeAttributeQuotes: true,
|
||||||
|
ignoreCustomComments: [/server-generated-meta/]
|
||||||
// more options:
|
// more options:
|
||||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
// see http://vuejs-templates.github.io/webpack for documentation.
|
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
let settings = {}
|
||||||
|
try {
|
||||||
|
settings = require('./local.json')
|
||||||
|
console.log('Using local dev server settings (/config/local.json):')
|
||||||
|
console.log(JSON.stringify(settings, null, 2))
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Local dev server settings not found (/config/local.json)')
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = settings.target || 'http://localhost:4000/'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
build: {
|
build: {
|
||||||
|
@ -19,21 +29,22 @@ module.exports = {
|
||||||
dev: {
|
dev: {
|
||||||
env: require('./dev.env'),
|
env: require('./dev.env'),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
|
settings,
|
||||||
assetsSubDirectory: 'static',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '/',
|
assetsPublicPath: '/',
|
||||||
proxyTable: {
|
proxyTable: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:4000/',
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost'
|
cookieDomainRewrite: 'localhost'
|
||||||
},
|
},
|
||||||
'/nodeinfo': {
|
'/nodeinfo': {
|
||||||
target: 'http://localhost:4000/',
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost'
|
cookieDomainRewrite: 'localhost'
|
||||||
},
|
},
|
||||||
'/socket': {
|
'/socket': {
|
||||||
target: 'http://localhost:4000/',
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost',
|
cookieDomainRewrite: 'localhost',
|
||||||
ws: true
|
ws: true
|
||||||
|
|
4
config/local.example.json
Normal file
4
config/local.example.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"target": "https://pleroma.soykaf.com/",
|
||||||
|
"staticConfigPreference": false
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Pleroma</title>
|
<title>Pleroma</title>
|
||||||
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
||||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
"serviceworker-webpack-plugin": "0.2.3",
|
||||||
"shelljs": "^0.7.4",
|
"shelljs": "^0.7.4",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
|
|
48
src/App.scss
48
src/App.scss
|
@ -228,24 +228,23 @@ i[class*=icon-] {
|
||||||
padding: 0 10px 0 10px;
|
padding: 0 10px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gaps {
|
|
||||||
margin: -1em 0 0 -1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
margin-left: 0.4em;
|
margin-left: 0.4em;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.gaps > .item {
|
&.right {
|
||||||
padding: 1em 0 0 1em;
|
justify-content: flex-end;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-size {
|
.auto-size {
|
||||||
|
@ -293,8 +292,6 @@ nav {
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-nav {
|
.inner-nav {
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-basis: 970px;
|
flex-basis: 970px;
|
||||||
|
@ -452,6 +449,23 @@ nav {
|
||||||
color: var(--faint, $fallback--faint);
|
color: var(--faint, $fallback--faint);
|
||||||
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||||
box-shadow: var(--topBarShadow);
|
box-shadow: var(--topBarShadow);
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: block;
|
||||||
|
max-width: 99px;
|
||||||
|
transition-property: opacity, max-width;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
max-width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
@ -486,6 +500,7 @@ nav {
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -499,6 +514,16 @@ nav {
|
||||||
body {
|
body {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
.back-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.site-name {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-bounds {
|
.sidebar-bounds {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
|
@ -591,11 +616,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.right {
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visibility-tray {
|
.visibility-tray {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|
|
@ -7,7 +7,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class='inner-nav'>
|
<div class='inner-nav'>
|
||||||
<div class='item'>
|
<div class='item'>
|
||||||
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
|
<router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden">
|
||||||
|
<i class="icon-left-open" :title="$t('nav.back')"></i>
|
||||||
|
</router-link>
|
||||||
|
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class='item right'>
|
<div class='item right'>
|
||||||
<user-finder class="nav-icon"></user-finder>
|
<user-finder class="nav-icon"></user-finder>
|
||||||
|
|
|
@ -17,17 +17,29 @@ import FollowRequests from '../components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from '../components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from '../components/oauth_callback/oauth_callback.vue'
|
||||||
import UserSearch from '../components/user_search/user_search.vue'
|
import UserSearch from '../components/user_search/user_search.vue'
|
||||||
|
|
||||||
const afterStoreSetup = ({store, i18n}) => {
|
const afterStoreSetup = ({ store, i18n }) => {
|
||||||
window.fetch('/api/statusnet/config.json')
|
window.fetch('/api/statusnet/config.json')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const {name, closed: registrationClosed, textlimit, server} = data.site
|
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
|
if (data.nsfwCensorImage) {
|
||||||
|
store.dispatch('setInstanceOption', { name: 'nsfwCensorImage', value: data.nsfwCensorImage })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vapidPublicKey) {
|
||||||
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
|
}
|
||||||
|
|
||||||
var apiConfig = data.site.pleromafe
|
var apiConfig = data.site.pleromafe
|
||||||
|
|
||||||
window.fetch('/static/config.json')
|
window.fetch('/static/config.json')
|
||||||
|
@ -38,8 +50,17 @@ const afterStoreSetup = ({store, i18n}) => {
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
.then((staticConfig) => {
|
.then((staticConfig) => {
|
||||||
|
const overrides = window.___pleromafe_dev_overrides || {}
|
||||||
|
const env = window.___pleromafe_mode.NODE_ENV
|
||||||
|
|
||||||
// This takes static config and overrides properties that are present in apiConfig
|
// This takes static config and overrides properties that are present in apiConfig
|
||||||
var config = Object.assign({}, staticConfig, apiConfig)
|
let config = {}
|
||||||
|
if (overrides.staticConfigPreference && env === 'development') {
|
||||||
|
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
|
||||||
|
config = Object.assign({}, apiConfig, staticConfig)
|
||||||
|
} else {
|
||||||
|
config = Object.assign({}, staticConfig, apiConfig)
|
||||||
|
}
|
||||||
|
|
||||||
var theme = (config.theme)
|
var theme = (config.theme)
|
||||||
var background = (config.background)
|
var background = (config.background)
|
||||||
|
|
|
@ -11,7 +11,7 @@ const Attachment = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
nsfwImage,
|
nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
|
||||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||||
preloadImage: this.$store.state.config.preloadImage,
|
preloadImage: this.$store.state.config.preloadImage,
|
||||||
loopVideo: this.$store.state.config.loopVideo,
|
loopVideo: this.$store.state.config.loopVideo,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||||
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
const mediaUpload = {
|
const mediaUpload = {
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -21,6 +22,12 @@ const mediaUpload = {
|
||||||
uploadFile (file) {
|
uploadFile (file) {
|
||||||
const self = this
|
const self = this
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
|
if (file.size > store.state.instance.uploadlimit) {
|
||||||
|
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||||
|
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
|
||||||
|
self.$emit('upload-failed', 'file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
|
||||||
|
return
|
||||||
|
}
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('media', file)
|
formData.append('media', file)
|
||||||
|
|
||||||
|
@ -32,7 +39,7 @@ const mediaUpload = {
|
||||||
self.$emit('uploaded', fileData)
|
self.$emit('uploaded', fileData)
|
||||||
self.uploading = false
|
self.uploading = false
|
||||||
}, (error) => { // eslint-disable-line handle-callback-err
|
}, (error) => { // eslint-disable-line handle-callback-err
|
||||||
self.$emit('upload-failed')
|
self.$emit('upload-failed', 'default')
|
||||||
self.uploading = false
|
self.uploading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -262,6 +262,11 @@ const PostStatusForm = {
|
||||||
let index = this.newStatus.files.indexOf(fileInfo)
|
let index = this.newStatus.files.indexOf(fileInfo)
|
||||||
this.newStatus.files.splice(index, 1)
|
this.newStatus.files.splice(index, 1)
|
||||||
},
|
},
|
||||||
|
uploadFailed (errString, templateArgs) {
|
||||||
|
templateArgs = templateArgs || {}
|
||||||
|
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
||||||
|
this.enableSubmit()
|
||||||
|
},
|
||||||
disableSubmit () {
|
disableSubmit () {
|
||||||
this.submitDisabled = true
|
this.submitDisabled = true
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-bottom'>
|
<div class='form-bottom'>
|
||||||
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
|
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||||
|
|
||||||
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
|
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
|
||||||
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
|
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
|
||||||
|
|
|
@ -47,6 +47,7 @@ const settings = {
|
||||||
scopeCopyLocal: user.scopeCopy,
|
scopeCopyLocal: user.scopeCopy,
|
||||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||||
stopGifs: user.stopGifs,
|
stopGifs: user.stopGifs,
|
||||||
|
webPushNotificationsLocal: user.webPushNotifications,
|
||||||
loopSilentAvailable:
|
loopSilentAvailable:
|
||||||
// Firefox
|
// Firefox
|
||||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||||
|
@ -142,6 +143,10 @@ const settings = {
|
||||||
},
|
},
|
||||||
stopGifs (value) {
|
stopGifs (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||||
|
},
|
||||||
|
webPushNotificationsLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
|
||||||
|
if (value) this.$store.dispatch('registerPushNotifications')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,18 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{$t('settings.notifications')}}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
|
||||||
|
<label for="webPushNotifications">
|
||||||
|
{{$t('settings.enable_web_push_notifications')}}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.theme')" >
|
<div :label="$t('settings.theme')" >
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-heading-right">
|
<div class="media-heading-right">
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
|
<router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
|
||||||
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="visibility-icon" v-if="status.visibility">
|
<div class="visibility-icon" v-if="status.visibility">
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Status from '../status/status.vue'
|
||||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
|
import { throttle } from 'lodash'
|
||||||
|
|
||||||
const Timeline = {
|
const Timeline = {
|
||||||
props: [
|
props: [
|
||||||
|
@ -88,7 +89,7 @@ const Timeline = {
|
||||||
this.paused = false
|
this.paused = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchOlderStatuses () {
|
fetchOlderStatuses: throttle(function () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
const credentials = store.state.users.currentUser.credentials
|
const credentials = store.state.users.currentUser.credentials
|
||||||
store.commit('setLoading', { timeline: this.timelineName, value: true })
|
store.commit('setLoading', { timeline: this.timelineName, value: true })
|
||||||
|
@ -101,7 +102,7 @@ const Timeline = {
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
tag: this.tag
|
tag: this.tag
|
||||||
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
||||||
},
|
}, 1000, this),
|
||||||
fetchFollowers () {
|
fetchFollowers () {
|
||||||
const id = this.userId
|
const id = this.userId
|
||||||
this.$store.state.api.backendInteractor.fetchFollowers({ id })
|
this.$store.state.api.backendInteractor.fetchFollowers({ id })
|
||||||
|
|
|
@ -14,6 +14,9 @@ const UserCard = {
|
||||||
components: {
|
components: {
|
||||||
UserCardContent
|
UserCardContent
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
currentUser () { return this.$store.state.users.currentUser }
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
this.userExpanded = !this.userExpanded
|
this.userExpanded = !this.userExpanded
|
||||||
|
|
|
@ -10,13 +10,13 @@
|
||||||
<div :title="user.name" v-if="user.name_html" class="user-name">
|
<div :title="user.name" v-if="user.name_html" class="user-name">
|
||||||
<span v-html="user.name_html"></span>
|
<span v-html="user.name_html"></span>
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
||||||
{{ $t('user_card.follows_you') }}
|
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :title="user.name" v-else class="user-name">
|
<div :title="user.name" v-else class="user-name">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
||||||
{{ $t('user_card.follows_you') }}
|
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">
|
<router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">
|
||||||
|
|
|
@ -22,10 +22,20 @@ export default {
|
||||||
if (color) {
|
if (color) {
|
||||||
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
|
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
|
||||||
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
|
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
|
||||||
|
|
||||||
|
const gradient = [
|
||||||
|
[tintColor, this.hideBio ? '60%' : ''],
|
||||||
|
this.hideBio ? [
|
||||||
|
color, '100%'
|
||||||
|
] : [
|
||||||
|
tintColor, ''
|
||||||
|
]
|
||||||
|
].map(_ => _.join(' ')).join(', ')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
|
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
|
||||||
backgroundImage: [
|
backgroundImage: [
|
||||||
`linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
|
`linear-gradient(to bottom, ${gradient})`,
|
||||||
`url(${this.user.cover_photo})`
|
`url(${this.user.cover_photo})`
|
||||||
].join(', ')
|
].join(', ')
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body profile-panel-body" v-if="switcher">
|
<div class="panel-body profile-panel-body" v-if="!hideBio">
|
||||||
<div v-if="!hideUserStatsLocal || switcher" 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'}">
|
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
||||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||||
|
@ -135,6 +135,9 @@
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
padding: 0.6em 0em;
|
padding: 0.6em 0em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="user-finder-container">
|
<div class="user-finder-container">
|
||||||
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
|
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
|
||||||
<a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
|
<a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
|
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
|
||||||
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
|
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_finder.js"></script>
|
<script src="./user_finder.js"></script>
|
||||||
|
@ -15,7 +15,6 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.user-finder-container {
|
.user-finder-container {
|
||||||
height: 29px;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,16 @@
|
||||||
<div v-if="user" class="user-profile panel panel-default">
|
<div v-if="user" class="user-profile panel panel-default">
|
||||||
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
|
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="panel user-profile-placeholder">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t('settings.profile_tab') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
|
<Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,4 +31,12 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.user-profile-placeholder {
|
||||||
|
.panel-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: middle;
|
||||||
|
padding: 7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||||
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
const UserSettings = {
|
const UserSettings = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
newname: this.$store.state.users.currentUser.name,
|
newName: this.$store.state.users.currentUser.name,
|
||||||
newbio: this.$store.state.users.currentUser.description,
|
newBio: this.$store.state.users.currentUser.description,
|
||||||
newlocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
newnorichtext: this.$store.state.users.currentUser.no_rich_text,
|
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||||
newdefaultScope: this.$store.state.users.currentUser.default_scope,
|
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||||
|
newHideNetwork: this.$store.state.users.currentUser.hide_network,
|
||||||
followList: null,
|
followList: null,
|
||||||
followImportError: false,
|
followImportError: false,
|
||||||
followsImported: false,
|
followsImported: false,
|
||||||
enableFollowsExport: true,
|
enableFollowsExport: true,
|
||||||
uploading: [ false, false, false, false ],
|
avatarUploading: false,
|
||||||
previews: [ null, null, null ],
|
bannerUploading: false,
|
||||||
|
backgroundUploading: false,
|
||||||
|
followListUploading: false,
|
||||||
|
avatarPreview: null,
|
||||||
|
bannerPreview: null,
|
||||||
|
backgroundPreview: null,
|
||||||
|
avatarUploadError: null,
|
||||||
|
bannerUploadError: null,
|
||||||
|
backgroundUploadError: null,
|
||||||
deletingAccount: false,
|
deletingAccount: false,
|
||||||
deleteAccountConfirmPasswordInput: '',
|
deleteAccountConfirmPasswordInput: '',
|
||||||
deleteAccountError: false,
|
deleteAccountError: false,
|
||||||
|
@ -40,48 +50,67 @@ const UserSettings = {
|
||||||
},
|
},
|
||||||
vis () {
|
vis () {
|
||||||
return {
|
return {
|
||||||
public: { selected: this.newdefaultScope === 'public' },
|
public: { selected: this.newDefaultScope === 'public' },
|
||||||
unlisted: { selected: this.newdefaultScope === 'unlisted' },
|
unlisted: { selected: this.newDefaultScope === 'unlisted' },
|
||||||
private: { selected: this.newdefaultScope === 'private' },
|
private: { selected: this.newDefaultScope === 'private' },
|
||||||
direct: { selected: this.newdefaultScope === 'direct' }
|
direct: { selected: this.newDefaultScope === 'direct' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateProfile () {
|
updateProfile () {
|
||||||
const name = this.newname
|
const name = this.newname
|
||||||
const description = this.newbio
|
const description = this.newBio
|
||||||
const locked = this.newlocked
|
const locked = this.newLocked
|
||||||
|
// Backend notation.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
const default_scope = this.newdefaultScope
|
const default_scope = this.newDefaultScope
|
||||||
const no_rich_text = this.newnorichtext
|
const no_rich_text = this.newNoRichText
|
||||||
this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope, no_rich_text}}).then((user) => {
|
const hide_network = this.newHideNetwork
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
this.$store.state.api.backendInteractor
|
||||||
|
.updateProfile({
|
||||||
|
params: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
locked,
|
||||||
|
// Backend notation.
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
default_scope,
|
||||||
|
no_rich_text,
|
||||||
|
hide_network
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}}).then((user) => {
|
||||||
if (!user.error) {
|
if (!user.error) {
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/* eslint-enable camelcase */
|
|
||||||
},
|
},
|
||||||
changeVis (visibility) {
|
changeVis (visibility) {
|
||||||
this.newdefaultScope = visibility
|
this.newDefaultScope = visibility
|
||||||
},
|
},
|
||||||
uploadFile (slot, e) {
|
uploadFile (slot, e) {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) { return }
|
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
|
// eslint-disable-next-line no-undef
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = ({target}) => {
|
reader.onload = ({target}) => {
|
||||||
const img = target.result
|
const img = target.result
|
||||||
this.previews[slot] = img
|
this[slot + 'Preview'] = img
|
||||||
this.$forceUpdate() // just changing the array with the index doesn't update the view
|
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
},
|
},
|
||||||
submitAvatar () {
|
submitAvatar () {
|
||||||
if (!this.previews[0]) { return }
|
if (!this.avatarPreview) { return }
|
||||||
|
|
||||||
let img = this.previews[0]
|
let img = this.avatarPreview
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
let imginfo = new Image()
|
let imginfo = new Image()
|
||||||
let cropX, cropY, cropW, cropH
|
let cropX, cropY, cropW, cropH
|
||||||
|
@ -97,20 +126,25 @@ const UserSettings = {
|
||||||
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
|
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
|
||||||
cropW = imginfo.height
|
cropW = imginfo.height
|
||||||
}
|
}
|
||||||
this.uploading[0] = true
|
this.avatarUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
|
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
|
||||||
if (!user.error) {
|
if (!user.error) {
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
this.previews[0] = null
|
this.avatarPreview = null
|
||||||
|
} else {
|
||||||
|
this.avatarUploadError = this.$t('upload.error.base') + user.error
|
||||||
}
|
}
|
||||||
this.uploading[0] = false
|
this.avatarUploading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
clearUploadError (slot) {
|
||||||
|
this[slot + 'UploadError'] = null
|
||||||
|
},
|
||||||
submitBanner () {
|
submitBanner () {
|
||||||
if (!this.previews[1]) { return }
|
if (!this.bannerPreview) { return }
|
||||||
|
|
||||||
let banner = this.previews[1]
|
let banner = this.bannerPreview
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
let imginfo = new Image()
|
let imginfo = new Image()
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -120,22 +154,24 @@ const UserSettings = {
|
||||||
height = imginfo.height
|
height = imginfo.height
|
||||||
offset_top = 0
|
offset_top = 0
|
||||||
offset_left = 0
|
offset_left = 0
|
||||||
this.uploading[1] = true
|
this.bannerUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
|
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
||||||
clone.cover_photo = data.url
|
clone.cover_photo = data.url
|
||||||
this.$store.commit('addNewUsers', [clone])
|
this.$store.commit('addNewUsers', [clone])
|
||||||
this.$store.commit('setCurrentUser', clone)
|
this.$store.commit('setCurrentUser', clone)
|
||||||
this.previews[1] = null
|
this.bannerPreview = null
|
||||||
|
} else {
|
||||||
|
this.bannerUploadError = this.$t('upload.error.base') + data.error
|
||||||
}
|
}
|
||||||
this.uploading[1] = false
|
this.bannerUploading = false
|
||||||
})
|
})
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
},
|
},
|
||||||
submitBg () {
|
submitBg () {
|
||||||
if (!this.previews[2]) { return }
|
if (!this.backgroundPreview) { return }
|
||||||
let img = this.previews[2]
|
let img = this.backgroundPreview
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
let imginfo = new Image()
|
let imginfo = new Image()
|
||||||
let cropX, cropY, cropW, cropH
|
let cropX, cropY, cropW, cropH
|
||||||
|
@ -144,20 +180,22 @@ const UserSettings = {
|
||||||
cropY = 0
|
cropY = 0
|
||||||
cropW = imginfo.width
|
cropW = imginfo.width
|
||||||
cropH = imginfo.width
|
cropH = imginfo.width
|
||||||
this.uploading[2] = true
|
this.backgroundUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
|
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
||||||
clone.background_image = data.url
|
clone.background_image = data.url
|
||||||
this.$store.commit('addNewUsers', [clone])
|
this.$store.commit('addNewUsers', [clone])
|
||||||
this.$store.commit('setCurrentUser', clone)
|
this.$store.commit('setCurrentUser', clone)
|
||||||
this.previews[2] = null
|
this.backgroundPreview = null
|
||||||
|
} else {
|
||||||
|
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||||
}
|
}
|
||||||
this.uploading[2] = false
|
this.backgroundUploading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
importFollows () {
|
importFollows () {
|
||||||
this.uploading[3] = true
|
this.followListUploading = true
|
||||||
const followList = this.followList
|
const followList = this.followList
|
||||||
this.$store.state.api.backendInteractor.followImport({params: followList})
|
this.$store.state.api.backendInteractor.followImport({params: followList})
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
|
@ -166,7 +204,7 @@ const UserSettings = {
|
||||||
} else {
|
} else {
|
||||||
this.followImportError = true
|
this.followImportError = true
|
||||||
}
|
}
|
||||||
this.uploading[3] = false
|
this.followListUploading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/* This function takes an Array of Users
|
/* This function takes an Array of Users
|
||||||
|
@ -198,6 +236,7 @@ const UserSettings = {
|
||||||
.fetchFriends({id: this.$store.state.users.currentUser.id})
|
.fetchFriends({id: this.$store.state.users.currentUser.id})
|
||||||
.then((friendList) => {
|
.then((friendList) => {
|
||||||
this.exportPeople(friendList, 'friends.csv')
|
this.exportPeople(friendList, 'friends.csv')
|
||||||
|
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
followListChange () {
|
followListChange () {
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
<div class="setting-item" >
|
<div class="setting-item" >
|
||||||
<h2>{{$t('settings.name_bio')}}</h2>
|
<h2>{{$t('settings.name_bio')}}</h2>
|
||||||
<p>{{$t('settings.name')}}</p>
|
<p>{{$t('settings.name')}}</p>
|
||||||
<input class='name-changer' id='username' v-model="newname"></input>
|
<input class='name-changer' id='username' v-model="newName"></input>
|
||||||
<p>{{$t('settings.bio')}}</p>
|
<p>{{$t('settings.bio')}}</p>
|
||||||
<textarea class="bio" v-model="newbio"></textarea>
|
<textarea class="bio" v-model="newBio"></textarea>
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="newlocked" id="account-locked">
|
<input type="checkbox" v-model="newLocked" id="account-locked">
|
||||||
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
||||||
</p>
|
</p>
|
||||||
<div v-if="scopeOptionsEnabled">
|
<div v-if="scopeOptionsEnabled">
|
||||||
|
@ -26,47 +26,63 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="newnorichtext" id="account-no-rich-text">
|
<input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
|
||||||
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
|
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
|
||||||
</p>
|
</p>
|
||||||
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
<p>
|
||||||
|
<input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
|
||||||
|
<label for="account-no-rich-text">{{$t('settings.hide_network_description')}}</label>
|
||||||
|
</p>
|
||||||
|
<button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.avatar')}}</h2>
|
<h2>{{$t('settings.avatar')}}</h2>
|
||||||
<p>{{$t('settings.current_avatar')}}</p>
|
<p>{{$t('settings.current_avatar')}}</p>
|
||||||
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
||||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||||
<img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
|
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
|
||||||
</img>
|
</img>
|
||||||
<div>
|
<div>
|
||||||
<input type="file" @change="uploadFile(0, $event)" ></input>
|
<input type="file" @change="uploadFile('avatar', $event)" ></input>
|
||||||
|
</div>
|
||||||
|
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
|
||||||
|
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
|
||||||
|
<div class='alert error' v-if="avatarUploadError">
|
||||||
|
Error: {{ avatarUploadError }}
|
||||||
|
<i class="icon-cancel" @click="clearUploadError('avatar')"></i>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||||
<p>{{$t('settings.current_profile_banner')}}</p>
|
<p>{{$t('settings.current_profile_banner')}}</p>
|
||||||
<img :src="user.cover_photo" class="banner"></img>
|
<img :src="user.cover_photo" class="banner"></img>
|
||||||
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
||||||
<img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
|
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
|
||||||
</img>
|
</img>
|
||||||
<div>
|
<div>
|
||||||
<input type="file" @change="uploadFile(1, $event)" ></input>
|
<input type="file" @change="uploadFile('banner', $event)" ></input>
|
||||||
|
</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="icon-cancel" @click="clearUploadError('banner')"></i>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_background')}}</h2>
|
<h2>{{$t('settings.profile_background')}}</h2>
|
||||||
<p>{{$t('settings.set_new_profile_background')}}</p>
|
<p>{{$t('settings.set_new_profile_background')}}</p>
|
||||||
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
|
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
|
||||||
</img>
|
</img>
|
||||||
<div>
|
<div>
|
||||||
<input type="file" @change="uploadFile(2, $event)" ></input>
|
<input type="file" @change="uploadFile('background', $event)" ></input>
|
||||||
|
</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="icon-cancel" @click="clearUploadError('background')"></i>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -113,7 +129,7 @@
|
||||||
<form v-model="followImportForm">
|
<form v-model="followImportForm">
|
||||||
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
||||||
</form>
|
</form>
|
||||||
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
|
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||||
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
||||||
<div v-if="followsImported">
|
<div v-if="followsImported">
|
||||||
<i class="icon-cross" @click="dismissImported"></i>
|
<i class="icon-cross" @click="dismissImported"></i>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
|
"back": "Back",
|
||||||
"chat": "Local Chat",
|
"chat": "Local Chat",
|
||||||
"friend_requests": "Follow Requests",
|
"friend_requests": "Follow Requests",
|
||||||
"mentions": "Mentions",
|
"mentions": "Mentions",
|
||||||
|
@ -133,7 +134,7 @@
|
||||||
"inputRadius": "Input fields",
|
"inputRadius": "Input fields",
|
||||||
"checkboxRadius": "Checkboxes",
|
"checkboxRadius": "Checkboxes",
|
||||||
"instance_default": "(default: {value})",
|
"instance_default": "(default: {value})",
|
||||||
"instance_default_simple" : "(default)",
|
"instance_default_simple": "(default)",
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
"interfaceLanguage": "Interface language",
|
"interfaceLanguage": "Interface language",
|
||||||
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
||||||
|
@ -151,6 +152,7 @@
|
||||||
"notification_visibility_mentions": "Mentions",
|
"notification_visibility_mentions": "Mentions",
|
||||||
"notification_visibility_repeats": "Repeats",
|
"notification_visibility_repeats": "Repeats",
|
||||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||||
|
"hide_network_description": "Don't show who I'm following and who's following me",
|
||||||
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
|
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
|
||||||
"panelRadius": "Panels",
|
"panelRadius": "Panels",
|
||||||
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
||||||
|
@ -190,6 +192,8 @@
|
||||||
"false": "no",
|
"false": "no",
|
||||||
"true": "yes"
|
"true": "yes"
|
||||||
},
|
},
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"enable_web_push_notifications": "Enable web push notifications",
|
||||||
"style": {
|
"style": {
|
||||||
"switcher": {
|
"switcher": {
|
||||||
"keep_color": "Keep colors",
|
"keep_color": "Keep colors",
|
||||||
|
@ -324,6 +328,7 @@
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
"following": "Following!",
|
"following": "Following!",
|
||||||
"follows_you": "Follows you!",
|
"follows_you": "Follows you!",
|
||||||
|
"its_you": "It's you!",
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"muted": "Muted",
|
"muted": "Muted",
|
||||||
"per_day": "per day",
|
"per_day": "per day",
|
||||||
|
@ -343,5 +348,19 @@
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
"user_settings": "User Settings"
|
"user_settings": "User Settings"
|
||||||
|
},
|
||||||
|
"upload":{
|
||||||
|
"error": {
|
||||||
|
"base": "Upload failed.",
|
||||||
|
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||||
|
"default": "Try again later"
|
||||||
|
},
|
||||||
|
"file_size_units": {
|
||||||
|
"B": "B",
|
||||||
|
"KiB": "KiB",
|
||||||
|
"MiB": "MiB",
|
||||||
|
"GiB": "GiB",
|
||||||
|
"TiB": "TiB"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"username": "Имя пользователя"
|
"username": "Имя пользователя"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
|
"back": "Назад",
|
||||||
"chat": "Локальный чат",
|
"chat": "Локальный чат",
|
||||||
"mentions": "Упоминания",
|
"mentions": "Упоминания",
|
||||||
"public_tl": "Публичная лента",
|
"public_tl": "Публичная лента",
|
||||||
|
@ -126,6 +127,7 @@
|
||||||
"notification_visibility_mentions": "Упоминания",
|
"notification_visibility_mentions": "Упоминания",
|
||||||
"notification_visibility_repeats": "Повторы",
|
"notification_visibility_repeats": "Повторы",
|
||||||
"no_rich_text_description": "Убрать форматирование из всех постов",
|
"no_rich_text_description": "Убрать форматирование из всех постов",
|
||||||
|
"hide_network_description": "Не показывать кого я читаю и кто меня читает",
|
||||||
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
|
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
|
||||||
"panelRadius": "Панели",
|
"panelRadius": "Панели",
|
||||||
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
|
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
|
||||||
|
|
36
src/main.js
36
src/main.js
|
@ -50,6 +50,32 @@ const persistedStateOptions = {
|
||||||
'oauth'
|
'oauth'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerPushNotifications = store => {
|
||||||
|
store.subscribe((mutation, state) => {
|
||||||
|
const vapidPublicKey = state.instance.vapidPublicKey
|
||||||
|
const permission = state.interface.notificationPermission === 'granted'
|
||||||
|
const isUserMutation = mutation.type === 'setCurrentUser'
|
||||||
|
|
||||||
|
if (isUserMutation && vapidPublicKey && permission) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = state.users.currentUser
|
||||||
|
const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
|
||||||
|
|
||||||
|
if (isVapidMutation && user && permission) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
|
||||||
|
|
||||||
|
if (isPermMutation && user && vapidPublicKey) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
createPersistedState(persistedStateOptions).then((persistedState) => {
|
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
@ -62,10 +88,16 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
chat: chatModule,
|
chat: chatModule,
|
||||||
oauth: oauthModule
|
oauth: oauthModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState],
|
plugins: [persistedState, registerPushNotifications],
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
|
||||||
afterStoreSetup({store, i18n})
|
afterStoreSetup({ store, i18n })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// These are inlined by webpack's DefinePlugin
|
||||||
|
/* eslint-disable */
|
||||||
|
window.___pleromafe_mode = process.env
|
||||||
|
window.___pleromafe_commit_hash = COMMIT_HASH
|
||||||
|
window.___pleromafe_dev_overrides = DEV_OVERRIDES
|
||||||
|
|
|
@ -24,6 +24,7 @@ const defaultState = {
|
||||||
likes: true,
|
likes: true,
|
||||||
repeats: true
|
repeats: true
|
||||||
},
|
},
|
||||||
|
webPushNotifications: true,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
highlight: {},
|
highlight: {},
|
||||||
interfaceLanguage: browserLocale,
|
interfaceLanguage: browserLocale,
|
||||||
|
|
|
@ -25,6 +25,8 @@ const defaultState = {
|
||||||
scopeCopy: true,
|
scopeCopy: true,
|
||||||
subjectLineBehavior: 'email',
|
subjectLineBehavior: 'email',
|
||||||
loginMethod: 'password',
|
loginMethod: 'password',
|
||||||
|
nsfwCensorImage: undefined,
|
||||||
|
vapidPublicKey: undefined,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
pleromaBackend: true,
|
pleromaBackend: true,
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { set, delete as del } from 'vue'
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null
|
noticeClearTimeout: null,
|
||||||
|
notificationPermission: null
|
||||||
},
|
},
|
||||||
browserSupport: {
|
browserSupport: {
|
||||||
cssFilter: window.CSS && window.CSS.supports && (
|
cssFilter: window.CSS && window.CSS.supports && (
|
||||||
|
@ -27,6 +28,9 @@ const interfaceMod = {
|
||||||
} else {
|
} else {
|
||||||
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setNotificationPermission (state, permission) {
|
||||||
|
state.notificationPermission = permission
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -35,6 +39,9 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||||
commit('settingsSaved', { success, error })
|
commit('settingsSaved', { success, error })
|
||||||
|
},
|
||||||
|
setNotificationPermission ({ commit }, permission) {
|
||||||
|
commit('setNotificationPermission', permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge } from 'lodash'
|
import { compact, map, each, merge } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
|
import registerPushNotifications from '../services/push/push.js'
|
||||||
import oauthApi from '../services/new_api/oauth'
|
import oauthApi from '../services/new_api/oauth'
|
||||||
import {humanizeErrors} from './errors'
|
import { humanizeErrors } from './errors'
|
||||||
|
|
||||||
// TODO: Unify with mergeOrAdd in statuses.js
|
// TODO: Unify with mergeOrAdd in statuses.js
|
||||||
export const mergeOrAdd = (arr, obj, item) => {
|
export const mergeOrAdd = (arr, obj, item) => {
|
||||||
|
@ -11,17 +12,28 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
// We already have this, so only merge the new info.
|
// We already have this, so only merge the new info.
|
||||||
merge(oldItem, item)
|
merge(oldItem, item)
|
||||||
return {item: oldItem, new: false}
|
return { item: oldItem, new: false }
|
||||||
} else {
|
} else {
|
||||||
// This is a new item, prepare it
|
// This is a new item, prepare it
|
||||||
arr.push(item)
|
arr.push(item)
|
||||||
obj[item.id] = item
|
obj[item.id] = item
|
||||||
return {item, new: true}
|
if (item.screen_name && !item.screen_name.includes('@')) {
|
||||||
|
obj[item.screen_name] = item
|
||||||
|
}
|
||||||
|
return { item, new: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNotificationPermission = () => {
|
||||||
|
const Notification = window.Notification
|
||||||
|
|
||||||
|
if (!Notification) return Promise.resolve(null)
|
||||||
|
if (Notification.permission === 'default') return Notification.requestPermission()
|
||||||
|
return Promise.resolve(Notification.permission)
|
||||||
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setMuted (state, { user: {id}, muted }) {
|
setMuted (state, { user: { id }, muted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
set(user, 'muted', muted)
|
set(user, 'muted', muted)
|
||||||
},
|
},
|
||||||
|
@ -45,7 +57,7 @@ export const mutations = {
|
||||||
setUserForStatus (state, status) {
|
setUserForStatus (state, status) {
|
||||||
status.user = state.usersObject[status.user.id]
|
status.user = state.usersObject[status.user.id]
|
||||||
},
|
},
|
||||||
setColor (state, { user: {id}, highlighted }) {
|
setColor (state, { user: { id }, highlighted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
set(user, 'highlight', highlighted)
|
set(user, 'highlight', highlighted)
|
||||||
},
|
},
|
||||||
|
@ -77,8 +89,15 @@ const users = {
|
||||||
mutations,
|
mutations,
|
||||||
actions: {
|
actions: {
|
||||||
fetchUser (store, id) {
|
fetchUser (store, id) {
|
||||||
store.rootState.api.backendInteractor.fetchUser({id})
|
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
.then((user) => store.commit('addNewUsers', user))
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
},
|
||||||
|
registerPushNotifications (store) {
|
||||||
|
const token = store.state.currentUser.credentials
|
||||||
|
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||||
|
const isEnabled = store.rootState.config.webPushNotifications
|
||||||
|
|
||||||
|
registerPushNotifications(isEnabled, vapidPublicKey, token)
|
||||||
},
|
},
|
||||||
addNewStatuses (store, { statuses }) {
|
addNewStatuses (store, { statuses }) {
|
||||||
const users = map(statuses, 'user')
|
const users = map(statuses, 'user')
|
||||||
|
@ -143,6 +162,9 @@ const users = {
|
||||||
commit('setCurrentUser', user)
|
commit('setCurrentUser', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
|
getNotificationPermission()
|
||||||
|
.then(permission => commit('setNotificationPermission', permission))
|
||||||
|
|
||||||
// Set our new backend interactor
|
// Set our new backend interactor
|
||||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||||
|
|
||||||
|
@ -161,12 +183,8 @@ const users = {
|
||||||
store.commit('addNewUsers', mutedUsers)
|
store.commit('addNewUsers', mutedUsers)
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'default') {
|
|
||||||
window.Notification.requestPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({id: user.id})
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
.then((friends) => commit('addNewUsers', friends))
|
.then((friends) => commit('addNewUsers', friends))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
17
src/services/file_size_format/file_size_format.js
Normal file
17
src/services/file_size_format/file_size_format.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const fileSizeFormat = (num) => {
|
||||||
|
var exponent
|
||||||
|
var unit
|
||||||
|
var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
|
||||||
|
if (num < 1) {
|
||||||
|
return num + ' ' + units[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
|
||||||
|
num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
|
||||||
|
unit = units[exponent]
|
||||||
|
return {num: num, unit: unit}
|
||||||
|
}
|
||||||
|
const fileSizeFormatService = {
|
||||||
|
fileSizeFormat
|
||||||
|
}
|
||||||
|
export default fileSizeFormatService
|
69
src/services/push/push.js
Normal file
69
src/services/push/push.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array (base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/')
|
||||||
|
|
||||||
|
const rawData = window.atob(base64)
|
||||||
|
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPushSupported () {
|
||||||
|
return 'serviceWorker' in navigator && 'PushManager' in window
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerServiceWorker () {
|
||||||
|
return runtime.register()
|
||||||
|
.catch((err) => console.error('Unable to register service worker.', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe (registration, isEnabled, vapidPublicKey) {
|
||||||
|
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
|
||||||
|
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
|
||||||
|
|
||||||
|
const subscribeOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
|
||||||
|
}
|
||||||
|
return registration.pushManager.subscribe(subscribeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSubscriptionToBackEnd (subscription, token) {
|
||||||
|
return window.fetch('/api/v1/push/subscription/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
subscription,
|
||||||
|
data: {
|
||||||
|
alerts: {
|
||||||
|
follow: true,
|
||||||
|
favourite: true,
|
||||||
|
mention: true,
|
||||||
|
reblog: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error('Bad status code from server.')
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseData) => {
|
||||||
|
if (!responseData.id) throw new Error('Bad response from server.')
|
||||||
|
return responseData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
|
||||||
|
if (isPushSupported()) {
|
||||||
|
registerServiceWorker()
|
||||||
|
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
|
||||||
|
.then((subscription) => sendSubscriptionToBackEnd(subscription, token))
|
||||||
|
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
||||||
|
}
|
||||||
|
}
|
38
src/sw.js
Normal file
38
src/sw.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/* eslint-env serviceworker */
|
||||||
|
|
||||||
|
import localForage from 'localforage'
|
||||||
|
|
||||||
|
function isEnabled () {
|
||||||
|
return localForage.getItem('vuex-lz')
|
||||||
|
.then(data => data.config.webPushNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindowClients () {
|
||||||
|
return clients.matchAll({ includeUncontrolled: true })
|
||||||
|
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
if (event.data) {
|
||||||
|
event.waitUntil(isEnabled().then((isEnabled) => {
|
||||||
|
return isEnabled && getWindowClients().then((list) => {
|
||||||
|
const data = event.data.json()
|
||||||
|
|
||||||
|
if (list.length === 0) return self.registration.showNotification(data.title, data)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close()
|
||||||
|
|
||||||
|
event.waitUntil(getWindowClients().then((list) => {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
var client = list[i]
|
||||||
|
if (client.url === '/' && 'focus' in client) { return client.focus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.openWindow) return clients.openWindow('/')
|
||||||
|
}))
|
||||||
|
})
|
|
@ -0,0 +1,34 @@
|
||||||
|
import fileSizeFormatService from '../../../../../src/services/file_size_format/file_size_format.js'
|
||||||
|
describe('fileSizeFormat', () => {
|
||||||
|
it('Formats file size', () => {
|
||||||
|
const values = [1, 1024, 1048576, 1073741824, 1099511627776]
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
num: 1,
|
||||||
|
unit: 'B'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: 1,
|
||||||
|
unit: 'KiB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: 1,
|
||||||
|
unit: 'MiB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: 1,
|
||||||
|
unit: 'GiB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: 1,
|
||||||
|
unit: 'TiB'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
var res = []
|
||||||
|
for (var value in values) {
|
||||||
|
res.push(fileSizeFormatService.fileSizeFormat(values[value]))
|
||||||
|
}
|
||||||
|
expect(res).to.eql(expected)
|
||||||
|
})
|
||||||
|
})
|
|
@ -3925,7 +3925,7 @@ mime@^1.3.4, mime@^1.5.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
|
||||||
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
|
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5289,6 +5289,12 @@ serve-static@1.13.1:
|
||||||
parseurl "~1.3.2"
|
parseurl "~1.3.2"
|
||||||
send "0.16.1"
|
send "0.16.1"
|
||||||
|
|
||||||
|
serviceworker-webpack-plugin@0.2.3:
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/serviceworker-webpack-plugin/-/serviceworker-webpack-plugin-0.2.3.tgz#1873ed6fc83c873ac8240fac443c615d374feeb2"
|
||||||
|
dependencies:
|
||||||
|
minimatch "^3.0.3"
|
||||||
|
|
||||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
|
|
Loading…
Reference in a new issue