Skip to content
Snippets Groups Projects
Verified Commit 37b6dd40 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'release/0.6'

parents 4530e4f4 6011cf20
No related branches found
No related tags found
No related merge requests found
Showing
with 305 additions and 30 deletions
...@@ -36,6 +36,9 @@ ...@@ -36,6 +36,9 @@
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> Login</router-link> <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> Login</router-link>
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>Browse library</router-link> <router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>Browse library</router-link>
<router-link class="item" :to="{path: '/favorites'}"><i class="heart icon"></i> Favorites</router-link> <router-link class="item" :to="{path: '/favorites'}"><i class="heart icon"></i> Favorites</router-link>
<router-link
v-if="$store.state.auth.authenticated"
class="item" :to="{path: '/activity'}"><i class="bell icon"></i> Activity</router-link>
</div> </div>
</div> </div>
<div v-if="queue.previousQueue " class="ui black icon message"> <div v-if="queue.previousQueue " class="ui black icon message">
......
<template>
<div class="event">
<div class="label">
<i class="pink heart icon"></i>
</div>
<div class="content">
<div class="summary">
<slot name="user"></slot>
favorited a track
<slot name="date"></slot>
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
</template>
<template v-else>, by <em>{{ event.object.artist }}</em>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['event']
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>
<template>
<div class="event">
<div class="label">
<i class="orange sound icon"></i>
</div>
<div class="content">
<div class="summary">
<slot name="user"></slot>
listened to a track
<slot name="date"></slot>
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
</template>
<template v-else>, by <em>{{ event.object.artist }}</em>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['event']
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>
...@@ -2,12 +2,35 @@ ...@@ -2,12 +2,35 @@
<div class="main pusher"> <div class="main pusher">
<div class="ui vertical stripe segment"> <div class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2>Change my password</h2> <h2 class="ui header">Account settings</h2>
<form class="ui form" @submit.prevent="submit()"> <form class="ui form" @submit.prevent="submitSettings()">
<div v-if="error" class="ui negative message"> <div v-if="settings.success" class="ui positive message">
<div class="header">Settings updated</div>
</div>
<div v-if="settings.errors.length > 0" class="ui negative message">
<div class="header">We cannot save your settings</div>
<ul class="list">
<li v-for="error in settings.errors">{{ error }}</li>
</ul>
</div>
<div class="field" v-for="f in orderedSettingsFields">
<label :for="f.id">{{ f.label }}</label>
<p v-if="f.help">{{ f.help }}</p>
<select v-if="f.type === 'dropdown'" class="ui dropdown" v-model="f.value">
<option :value="c.value" v-for="c in f.choices">{{ c.label }}</option>
</select>
</div>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Update settings</button>
</form>
</div>
<div class="ui hidden divider"></div>
<div class="ui small text container">
<h2 class="ui header">Change my password</h2>
<form class="ui form" @submit.prevent="submitPassword()">
<div v-if="passwordError" class="ui negative message">
<div class="header">Cannot change your password</div> <div class="header">Cannot change your password</div>
<ul class="list"> <ul class="list">
<li v-if="error == 'invalid_credentials'">Please double-check your password is correct</li> <li v-if="passwordError == 'invalid_credentials'">Please double-check your password is correct</li>
</ul> </ul>
</div> </div>
<div class="field"> <div class="field">
...@@ -36,22 +59,68 @@ ...@@ -36,22 +59,68 @@
</template> </template>
<script> <script>
import $ from 'jquery'
import axios from 'axios' import axios from 'axios'
import logger from '@/logging' import logger from '@/logging'
export default { export default {
data () { data () {
return { let d = {
// We need to initialize the component with any // We need to initialize the component with any
// properties that will be used in it // properties that will be used in it
old_password: '', old_password: '',
new_password: '', new_password: '',
error: '', passwordError: '',
isLoading: false isLoading: false,
settings: {
success: false,
errors: [],
order: ['privacy_level'],
fields: {
'privacy_level': {
type: 'dropdown',
initial: this.$store.state.auth.profile.privacy_level,
label: 'Activity visibility',
help: 'Determine the visibility level of your activity',
choices: [
{
value: 'me',
label: 'Nobody except me'
},
{
value: 'instance',
label: 'Everyone on this instance'
}
]
}
}
} }
}
d.settings.order.forEach(id => {
d.settings.fields[id].value = d.settings.fields[id].initial
})
return d
},
mounted () {
$('select.dropdown').dropdown()
}, },
methods: { methods: {
submit () { submitSettings () {
this.settings.success = false
this.settings.errors = []
let self = this
let payload = this.settingsValues
let url = `users/users/${this.$store.state.auth.username}/`
return axios.patch(url, payload).then(response => {
logger.default.info('Updated settings successfully')
self.settings.success = true
}, error => {
logger.default.error('Error while updating settings')
self.isLoading = false
self.settings.errors = error.backendErrors
})
},
submitPassword () {
var self = this var self = this
self.isLoading = true self.isLoading = true
this.error = '' this.error = ''
...@@ -70,13 +139,30 @@ export default { ...@@ -70,13 +139,30 @@ export default {
}}) }})
}, error => { }, error => {
if (error.response.status === 400) { if (error.response.status === 400) {
self.error = 'invalid_credentials' self.passwordError = 'invalid_credentials'
} else { } else {
self.error = 'unknown_error' self.passwordError = 'unknown_error'
} }
self.isLoading = false self.isLoading = false
}) })
} }
},
computed: {
orderedSettingsFields () {
let self = this
return this.settings.order.map(id => {
return self.settings.fields[id]
})
},
settingsValues () {
let self = this
let s = {}
this.settings.order.forEach(setting => {
let conf = self.settings.fields[setting]
s[setting] = conf.value
})
return s
}
} }
} }
......
...@@ -100,24 +100,9 @@ export default { ...@@ -100,24 +100,9 @@ export default {
username: this.username username: this.username
}}) }})
}, error => { }, error => {
self.errors = this.getErrors(error.response) self.errors = error.backendErrors
self.isLoading = false self.isLoading = false
}) })
},
getErrors (response) {
let errors = []
if (response.status !== 400) {
errors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
return errors
}
for (var field in response.data) {
if (response.data.hasOwnProperty(field)) {
response.data[field].forEach(e => {
errors.push(e)
})
}
}
return errors
} }
}, },
computed: { computed: {
......
<template> <template>
<time :datetime="date" :title="date | moment">{{ date | ago }}</time> <time :datetime="date" :title="date | moment">{{ realDate | ago }}</time>
</template> </template>
<script> <script>
import {mapState} from 'vuex'
export default { export default {
props: ['date'] props: ['date'],
computed: {
...mapState({
lastDate: state => state.ui.lastDate
}),
realDate () {
if (this.lastDate) {
// dummy code to trigger a recompute to update the ago render
}
return this.date
}
}
} }
</script> </script>
<template>
<span>{{ username }}</span>
</template>
<script>
export default {
props: ['username']
}
</script>
...@@ -4,4 +4,8 @@ import HumanDate from '@/components/common/HumanDate' ...@@ -4,4 +4,8 @@ import HumanDate from '@/components/common/HumanDate'
Vue.component('human-date', HumanDate) Vue.component('human-date', HumanDate)
import Username from '@/components/common/Username'
Vue.component('username', Username)
export default {} export default {}
...@@ -62,12 +62,18 @@ export default { ...@@ -62,12 +62,18 @@ export default {
data () { data () {
return { return {
isLoading: true, isLoading: true,
batch: null batch: null,
timeout: null
} }
}, },
created () { created () {
this.fetchData() this.fetchData()
}, },
destroyed () {
if (this.timeout) {
clearTimeout(this.timeout)
}
},
methods: { methods: {
fetchData () { fetchData () {
var self = this var self = this
...@@ -78,7 +84,7 @@ export default { ...@@ -78,7 +84,7 @@ export default {
self.batch = response.data self.batch = response.data
self.isLoading = false self.isLoading = false
if (self.batch.status === 'pending') { if (self.batch.status === 'pending') {
setTimeout( self.timeout = setTimeout(
self.fetchData, self.fetchData,
5000 5000
) )
......
...@@ -47,11 +47,28 @@ axios.interceptors.request.use(function (config) { ...@@ -47,11 +47,28 @@ axios.interceptors.request.use(function (config) {
axios.interceptors.response.use(function (response) { axios.interceptors.response.use(function (response) {
return response return response
}, function (error) { }, function (error) {
error.backendErrors = []
if (error.response.status === 401) { if (error.response.status === 401) {
store.commit('auth/authenticated', false) store.commit('auth/authenticated', false)
logger.default.warn('Received 401 response from API, redirecting to login form') logger.default.warn('Received 401 response from API, redirecting to login form')
router.push({name: 'login', query: {next: router.currentRoute.fullPath}}) router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
} }
if (error.response.status === 404) {
error.backendErrors.push('Resource not found')
} else if (error.response.status === 500) {
error.backendErrors.push('A server error occured')
} else if (error.response.data) {
for (var field in error.response.data) {
if (error.response.data.hasOwnProperty(field)) {
error.response.data[field].forEach(e => {
error.backendErrors.push(e)
})
}
}
}
if (error.backendErrors.length === 0) {
error.backendErrors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
}
// Do something with response error // Do something with response error
return Promise.reject(error) return Promise.reject(error)
}) })
......
...@@ -3,6 +3,7 @@ import Router from 'vue-router' ...@@ -3,6 +3,7 @@ import Router from 'vue-router'
import PageNotFound from '@/components/PageNotFound' import PageNotFound from '@/components/PageNotFound'
import About from '@/components/About' import About from '@/components/About'
import Home from '@/components/Home' import Home from '@/components/Home'
import InstanceTimeline from '@/views/instance/Timeline'
import Login from '@/components/auth/Login' import Login from '@/components/auth/Login'
import Signup from '@/components/auth/Signup' import Signup from '@/components/auth/Signup'
import Profile from '@/components/auth/Profile' import Profile from '@/components/auth/Profile'
...@@ -39,6 +40,11 @@ export default new Router({ ...@@ -39,6 +40,11 @@ export default new Router({
name: 'about', name: 'about',
component: About component: About
}, },
{
path: '/activity',
name: 'activity',
component: InstanceTimeline
},
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
......
...@@ -8,11 +8,13 @@ import instance from './instance' ...@@ -8,11 +8,13 @@ import instance from './instance'
import queue from './queue' import queue from './queue'
import radios from './radios' import radios from './radios'
import player from './player' import player from './player'
import ui from './ui'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
modules: { modules: {
ui,
auth, auth,
favorites, favorites,
instance, instance,
...@@ -28,6 +30,10 @@ export default new Vuex.Store({ ...@@ -28,6 +30,10 @@ export default new Vuex.Store({
return mutation.type.startsWith('auth/') return mutation.type.startsWith('auth/')
} }
}), }),
createPersistedState({
key: 'instance',
paths: ['instance.events']
}),
createPersistedState({ createPersistedState({
key: 'radios', key: 'radios',
paths: ['radios'], paths: ['radios'],
......
...@@ -5,6 +5,8 @@ import _ from 'lodash' ...@@ -5,6 +5,8 @@ import _ from 'lodash'
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
maxEvents: 200,
events: [],
settings: { settings: {
instance: { instance: {
name: { name: {
...@@ -35,6 +37,12 @@ export default { ...@@ -35,6 +37,12 @@ export default {
mutations: { mutations: {
settings: (state, value) => { settings: (state, value) => {
_.merge(state.settings, value) _.merge(state.settings, value)
},
event: (state, value) => {
state.events.unshift(value)
if (state.events.length > state.maxEvents) {
state.events = state.events.slice(0, state.maxEvents)
}
} }
}, },
actions: { actions: {
......
export default {
namespaced: true,
state: {
lastDate: new Date()
},
mutations: {
computeLastDate: (state) => {
state.lastDate = new Date()
}
}
}
<template>
<div class="main pusher">
<div class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui header">Recent activity on this instance</h1>
<div class="ui feed">
<component
class="event"
v-for="(event, index) in events"
:key="event.id + index"
v-if="components[event.type]"
:is="components[event.type]"
:event="event">
<username
class="user"
:username="event.actor.local_id"
slot="user"></username>
{{ event.published }}
<human-date class="date" :date="event.published" slot="date"></human-date>
</component>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import Like from '@/components/activity/Like'
import Listen from '@/components/activity/Listen'
export default {
data () {
return {
components: {
'Like': Like,
'Listen': Listen
}
}
},
computed: {
...mapState({
events: state => state.instance.events
})
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment