WIP: feat: account switching #279

Closed
Ghost wants to merge 1 commit from (deleted):feat/account-switching into develop
12 changed files with 195 additions and 19 deletions

View file

@ -349,7 +349,7 @@ const checkOAuthToken = async ({ store }) => {
return new Promise(async (resolve, reject) => {
if (store.getters.getUserToken()) {
try {
await store.dispatch('loginUser', store.getters.getUserToken())
await store.dispatch('loginUser', store.getters.getUserToken()[store.state.users.lastLoginName])
} catch (e) {
console.error(e)
}

View file

@ -0,0 +1,52 @@
import Popover from '../popover/popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from '../rich_content/rich_content.jsx'
import { map } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faRightToBracket,
faUserPen
} from '@fortawesome/free-solid-svg-icons'
library.add(
faRightToBracket,
faUserPen
)
const AccountSwitcher = {
components: {
Popover,
UserAvatar,
RichContent
},
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
accounts () {
return map(Object.keys(this.$store.state.oauth.userToken), username => (
this.$store.getters.findUser(username)
))
},
registrationOpen () {
return this.$store.state.instance.registrationOpen
}
},
methods: {
login () {
this.$store.commit('beginAccountSwitch')
this.$router.push({ name: 'login' })
},
switchAccount (username, token) {
// don't switch to same user
if (username !== this.currentUser.screen_name) {
this.$store.commit('beginAccountSwitch')
this.$store.dispatch('loginUser', this.$store.state.oauth.userToken[username]).then(() => {
this.$router.push({ name: 'friends' })
})
}
}
}
}
export default AccountSwitcher

View file

@ -0,0 +1,90 @@
<template>
<Popover
trigger="click"
class="SwitchAccounts"
:bound-to="{ x: 'container' }"
:offset="{ x: -16 }"
>
<template #trigger>
<button
class="button-unstyled switch-account-button"
>
<FAIcon
fixed-width
class="icon"
icon="user-plus"
:title="$t('user_card.switch_accounts')"
/>
</button>
</template>
<template #content>
<div class="dropdown-menu">
<button
v-for="account in accounts"
class="button-default dropdown-item account-button"
:class="account.id === currentUser.id ? 'selected' : ''"
:key="account.screen_name"
@click="switchAccount(account.screen_name)"
>
<UserAvatar
:compact="true"
:user="account"
/>
<div class="right-side">
<RichContent
class="username"
:title="'@'+account.screen_name"
:html="account.name_html"
:emoji="account.emoji"
/>
<a>@{{ account.screen_name }}</a>
</div>
</button>
<div
role="separator"
class="dropdown-divider"
/>
<button
class="button-default dropdown-item dropdown-item-icon"
@click="login"
>
<FAIcon icon="right-to-bracket" />{{ $t('login.login') }}
</button>
<button
v-if="registrationOpen"
class="button-default dropdown-item dropdown-item-icon"
@click="register"
>
<FAIcon icon="user-pen" />{{ $t('login.register') }}
</button>
</div>
</template>
</Popover>
</template>
<script src="./account_switcher.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.button-default.dropdown-item.account-button {
display: flex;
&.selected {
background-color: var(--selectedPost, $fallback--lightBg);
}
.Avatar {
margin-right: 0.75em;
}
.right-side {
margin: auto;
}
.username {
font-weight: bolder;
margin-right: 0.4em;
}
}
</style>

View file

@ -75,7 +75,7 @@ const LoginForm = {
}
return
}
this.login(result).then(() => {
this.login({ username: this.user.username, ...result }).then(() => {
this.$router.push({ name: 'friends' })
})
})

View file

@ -6,6 +6,7 @@ const oac = {
if (this.code) {
const { clientId, clientSecret } = this.$store.state.oauth
// XXX look at this later
oauth.getToken({
clientId,
clientSecret,

View file

@ -69,9 +69,16 @@ const SettingsModal = {
this.$store.dispatch('closeSettingsModal')
},
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('closeSettingsModal')
this.$router.replace('/main/public')
this.$store.dispatch('logout')
.then(() => {
// check if logged in to other accounts
const accounts = Object.keys(this.$store.state.oauth.userToken)
if (accounts !== []) {
this.$store.dispatch('loginUser', this.$store.state.oauth.userToken[accounts[0]])
}
})
},
peekModal () {
this.$store.dispatch('togglePeekSettingsModal')

View file

@ -7,6 +7,7 @@ import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import AccountSwitcher from '../account_switcher/account_switcher.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -15,7 +16,8 @@ import {
faRss,
faSearchPlus,
faExternalLinkAlt,
faEdit
faEdit,
faUserPlus,
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -23,7 +25,8 @@ library.add(
faBell,
faSearchPlus,
faExternalLinkAlt,
faEdit
faEdit,
faUserPlus,
)
export default {
@ -128,7 +131,8 @@ export default {
FollowButton,
Select,
RichContent,
ConfirmModal
ConfirmModal,
AccountSwitcher
},
methods: {
refetchRelationship () {

View file

@ -110,7 +110,7 @@
min-width: 0;
}
.Avatar {
a .Avatar {
--_avatarShadowBox: var(--avatarShadow);
--_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset);
@ -151,7 +151,7 @@
}
}
.external-link-button, .edit-profile-button {
.external-link-button, .edit-profile-button, .switch-account-button {
cursor: pointer;
width: 2.5em;
text-align: center;

View file

@ -56,6 +56,7 @@
:title="$t('user_card.edit_profile')"
/>
</button>
<AccountSwitcher v-if="!isOtherUser && user.is_local" />
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"

View file

@ -68,8 +68,8 @@ const mutations = {
// actions
const actions = {
// eslint-disable-next-line camelcase
async login ({ state, dispatch, commit }, { access_token }) {
commit('setToken', access_token, { root: true })
async login ({ state, dispatch, commit }, { access_token, username }) {
commit('setToken', { token: access_token, username }, { root: true })
await dispatch('loginUser', access_token, { root: true })
resetState(state)
}

View file

@ -10,7 +10,7 @@ const oauth = {
/* User token is authentication for app with user, this is for every calls
* that need authorized user to be successful (i.e. posting, liking etc)
*/
userToken: false
userToken: {}
},
mutations: {
setClientData (state, { clientId, clientSecret }) {
@ -20,11 +20,11 @@ const oauth = {
setAppToken (state, token) {
state.appToken = token
},
setToken (state, token) {
state.userToken = token
setToken (state, { token, username }) {
state.userToken[username] = token
},
clearToken (state) {
state.userToken = false
clearToken (state, username) {
delete state.userToken[username]
// state.token is userToken with older name, coming from persistent state
// let's clear it as well, since it is being used as a fallback of state.userToken
delete state.token

View file

@ -149,6 +149,12 @@ export const mutations = {
endLogin (state) {
state.loggingIn = false
},
beginAccountSwitch (state) {
state.switchingAccounts = true
},
endAccountSwitch (state) {
state.switchingAccounts = false
},
saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = uniq(concat(user.friendIds || [], friendIds))
@ -296,6 +302,7 @@ export const getters = {
export const defaultState = {
loggingIn: false,
switchingAccounts: false,
lastLoginName: false,
currentUser: false,
users: [],
@ -537,7 +544,7 @@ const users = {
return data
} else if (data.me !== undefined) {
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.commit('setToken', { token: data.access_token, username: userInfo.nickname })
store.dispatch('loginUser', data.access_token)
return data
} else {
@ -554,12 +561,13 @@ const users = {
},
logout (store) {
const { oauth, instance } = store.rootState
const { oauth, instance, users } = store.rootState
const data = {
...oauth,
commit: store.commit,
instance: instance.server
instance: instance.server,
username: users.currentUser.screen_name
}
return oauthApi.getOrCreateApp(data)
@ -575,7 +583,7 @@ const users = {
.then(() => {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.commit('clearToken')
store.commit('clearToken', data.username)
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
@ -594,6 +602,19 @@ const users = {
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((data) => {
if (!data.error) {
// clear old user
// TODO: maybe we don't need some of this
if (store.state.loggingIn) {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.dispatch('stopFetchingTimeline', 'friends')
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingConfig')
store.commit('clearNotifications')
store.commit('resetStatuses')
store.commit('endAccountSwitch')
}
const user = data
// user.credentials = userCredentials
user.credentials = accessToken