Verified Commit 37b6dd40 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'release/0.6'

parents 4530e4f4 6011cf20
......@@ -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" :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
v-if="$store.state.auth.authenticated"
class="item" :to="{path: '/activity'}"><i class="bell icon"></i> Activity</router-link>
</div>
</div>
<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 @@
<div class="main pusher">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>Change my password</h2>
<form class="ui form" @submit.prevent="submit()">
<div v-if="error" class="ui negative message">
<h2 class="ui header">Account settings</h2>
<form class="ui form" @submit.prevent="submitSettings()">
<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>
<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>
</div>
<div class="field">
......@@ -36,22 +59,68 @@
</template>
<script>
import $ from 'jquery'
import axios from 'axios'
import logger from '@/logging'
export default {
data () {
return {
let d = {
// We need to initialize the component with any
// properties that will be used in it
old_password: '',
new_password: '',
error: '',
isLoading: false
passwordError: '',
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: {
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
self.isLoading = true
this.error = ''
......@@ -70,13 +139,30 @@ export default {
}})
}, error => {
if (error.response.status === 400) {
self.error = 'invalid_credentials'
self.passwordError = 'invalid_credentials'
} else {
self.error = 'unknown_error'
self.passwordError = 'unknown_error'
}
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 {
username: this.username
}})
}, error => {
self.errors = this.getErrors(error.response)
self.errors = error.backendErrors
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: {
......
<template>
<time :datetime="date" :title="date | moment">{{ date | ago }}</time>
<time :datetime="date" :title="date | moment">{{ realDate | ago }}</time>
</template>
<script>
import {mapState} from 'vuex'
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>
<template>
<span>{{ username }}</span>
</template>
<script>
export default {
props: ['username']
}
</script>
......@@ -4,4 +4,8 @@ import HumanDate from '@/components/common/HumanDate'
Vue.component('human-date', HumanDate)
import Username from '@/components/common/Username'
Vue.component('username', Username)
export default {}
......@@ -62,12 +62,18 @@ export default {
data () {
return {
isLoading: true,
batch: null
batch: null,
timeout: null
}
},
created () {
this.fetchData()
},
destroyed () {
if (this.timeout) {
clearTimeout(this.timeout)
}
},
methods: {
fetchData () {
var self = this
......@@ -78,7 +84,7 @@ export default {
self.batch = response.data
self.isLoading = false
if (self.batch.status === 'pending') {
setTimeout(
self.timeout = setTimeout(
self.fetchData,
5000
)
......
......@@ -47,11 +47,28 @@ axios.interceptors.request.use(function (config) {
axios.interceptors.response.use(function (response) {
return response
}, function (error) {
error.backendErrors = []
if (error.response.status === 401) {
store.commit('auth/authenticated', false)
logger.default.warn('Received 401 response from API, redirecting to login form')
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
return Promise.reject(error)
})
......
......@@ -3,6 +3,7 @@ import Router from 'vue-router'
import PageNotFound from '@/components/PageNotFound'
import About from '@/components/About'
import Home from '@/components/Home'
import InstanceTimeline from '@/views/instance/Timeline'
import Login from '@/components/auth/Login'
import Signup from '@/components/auth/Signup'
import Profile from '@/components/auth/Profile'
......@@ -39,6 +40,11 @@ export default new Router({
name: 'about',
component: About
},
{
path: '/activity',
name: 'activity',
component: InstanceTimeline
},
{
path: '/login',
name: 'login',
......
......@@ -8,11 +8,13 @@ import instance from './instance'
import queue from './queue'
import radios from './radios'
import player from './player'
import ui from './ui'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
ui,
auth,
favorites,
instance,
......@@ -28,6 +30,10 @@ export default new Vuex.Store({
return mutation.type.startsWith('auth/')
}
}),
createPersistedState({
key: 'instance',
paths: ['instance.events']
}),
createPersistedState({
key: 'radios',
paths: ['radios'],
......
......@@ -5,6 +5,8 @@ import _ from 'lodash'
export default {
namespaced: true,
state: {
maxEvents: 200,
events: [],
settings: {
instance: {
name: {
......@@ -35,6 +37,12 @@ export default {
mutations: {
settings: (state, 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: {
......
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>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment