Merge branch 'mastoapi/user-stuff' into shigusegubu

* mastoapi/user-stuff:
  whoops
  レインせんぱいにサンキュー
  fix embedded relationship card parsing
  actually use embedded relationship if it's present
  instead of filtering nulls, let's just not have them in the first place
  fixed tests, review fixes, now storing local users with downcase screen name for better compatibility
  fix error
  some test fixes, disabled one test for now since logic now is even more async in general
  attempt at fixing switching to user TL
  fix reply-to marker, also whoops console log
  revert some stuff, turns out it's actually breaking. Fixed some local user things
  Since BE doesn't support fetching user by screen name over MastoAPI we'll gonna just fetching it over QvitterAPI real quick :DDDDDDDDD
  switch to mastoapi for user timeline
  Partially transitioned user data to MastoAPI. Added support for fetching relationship data. Upgraded code to be more resilient to nulls caused by missing data in either APIs
This commit is contained in:
Henry Jameson 2019-03-13 20:25:38 +02:00
commit c0908e238f
13 changed files with 153 additions and 107 deletions

View file

@ -9,7 +9,7 @@ const BlockCard = {
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
return this.$store.getters.findUser(this.userId)
},
blocked () {
return this.user.statusnet_blocking

View file

@ -9,7 +9,7 @@ const MuteCard = {
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
return this.$store.getters.findUser(this.userId)
},
muted () {
return this.user.muted

View file

@ -145,11 +145,11 @@ const Status = {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
replyToName () {
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
if (user) {
return user.screen_name
} else {
if (this.status.in_reply_to_screen_name) {
return this.status.in_reply_to_screen_name
} else {
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
return user && user.screen_name
}
},
hideReply () {

View file

@ -15,6 +15,9 @@ export default {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
created () {
this.$store.dispatch('fetchUserRelationship', this.user.id)
},
computed: {
classes () {
return [{

View file

@ -9,7 +9,7 @@ import withList from '../../hocs/with_list/with_list'
const FollowerList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []),
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
@ -20,7 +20,7 @@ const FollowerList = compose(
const FriendList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
@ -31,28 +31,16 @@ const FriendList = compose(
const UserProfile = {
data () {
return {
error: false
error: false,
fetchedUserId: null
}
},
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' })
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
this.startFetchFavorites()
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
this.fetchUserId()
.then(() => this.startUp())
} else {
this.startUp()
}
},
destroyed () {
@ -69,7 +57,7 @@ const UserProfile = {
return this.$store.state.statuses.timelines.media
},
userId () {
return this.$route.params.id || this.user.id
return this.$route.params.id || this.user.id || this.fetchedUserId
},
userName () {
return this.$route.params.name || this.user.screen_name
@ -79,10 +67,8 @@ const UserProfile = {
this.userId === this.$store.state.users.currentUser.id
},
userInStore () {
if (this.isExternal) {
return this.$store.getters.userById(this.userId)
}
return this.$store.getters.userByName(this.userName)
const routeParams = this.$route.params
return this.$store.getters.findUser(routeParams.name || routeParams.id)
},
user () {
if (this.timeline.statuses[0]) {
@ -93,9 +79,6 @@ const UserProfile = {
}
return {}
},
fetchBy () {
return this.isExternal ? this.userId : this.userName
},
isExternal () {
return this.$route.name === 'external-user-profile'
},
@ -109,14 +92,38 @@ const UserProfile = {
methods: {
startFetchFavorites () {
if (this.isUs) {
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId })
}
},
fetchUserId () {
let fetchPromise
if (this.userId && !this.$route.params.name) {
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
} else {
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
.then(({ id }) => {
this.fetchedUserId = id
})
}
return fetchPromise
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
.then(() => this.startUp())
},
startUp () {
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
this.startFetchFavorites()
if (this.userId) {
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId })
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId })
this.startFetchFavorites()
}
},
cleanUp () {
this.$store.dispatch('stopFetching', 'user')
@ -128,19 +135,19 @@ const UserProfile = {
}
},
watch: {
userName () {
if (this.isExternal) {
return
// userId can be undefined if we don't know it yet
userId (newVal) {
if (newVal) {
this.cleanUp()
this.startUp()
}
this.cleanUp()
this.startUp()
},
userId () {
if (!this.isExternal) {
return
userName () {
if (this.$route.params.name) {
this.fetchUserId()
this.cleanUp()
this.startUp()
}
this.cleanUp()
this.startUp()
},
$route () {
this.$refs.tabSwitcher.activateTab(0)()

View file

@ -11,7 +11,7 @@
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
:timeline-name="'user'"
:user-id="fetchBy"
:user-id="userId"
/>
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
<FriendList :userId="userId" />
@ -25,7 +25,7 @@
:embedded="true" :title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
:user-id="fetchBy"
:user-id="userId"
/>
<Timeline
v-if="isUs"

View file

@ -1,4 +1,4 @@
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray } from 'lodash'
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@ -72,7 +72,9 @@ const mergeOrAdd = (arr, obj, item) => {
if (oldItem) {
// We already have this, so only merge the new info.
merge(oldItem, item)
// We ignore null values to avoid overwriting existing properties with missing data
// we also skip 'user' because that is handled by users module
merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user'))
// Reactivity fix.
oldItem.attachments.splice(oldItem.attachments.length)
return {item: oldItem, new: false}

View file

@ -18,7 +18,7 @@ export const mergeOrAdd = (arr, obj, item) => {
arr.push(item)
obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name] = item
obj[item.screen_name.toLowerCase()] = item
}
return { item, new: true }
}
@ -91,6 +91,16 @@ export const mutations = {
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
const user = state.usersObject[relationship.id]
user.follows_you = relationship.followed_by
user.following = relationship.following
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
})
},
saveBlocks (state, blockIds) {
state.currentUser.blockIds = blockIds
},
@ -122,12 +132,7 @@ export const mutations = {
}
export const getters = {
userById: state => id =>
state.users.find(user => user.id === id),
userByName: state => name =>
state.users.find(user => user.screen_name &&
(user.screen_name.toLowerCase() === name.toLowerCase())
)
findUser: state => query => state.usersObject[typeof query === 'string' ? query.toLowerCase() : query]
}
export const defaultState = {
@ -147,7 +152,14 @@ const users = {
actions: {
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) {
return store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()

View file

@ -28,12 +28,10 @@ const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKING_URL = '/api/blocks/create.json'
const UNBLOCKING_URL = '/api/blocks/destroy.json'
const USER_URL = '/api/users/show.json'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
@ -43,6 +41,9 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
import { each, map } from 'lodash'
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@ -243,7 +244,7 @@ const denyUser = ({id, credentials}) => {
}
const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
let url = `${MASTODON_USER_URL}/${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
return new Promise((resolve, reject) => response.json()
@ -257,6 +258,20 @@ const fetchUser = ({id, credentials}) => {
.then((data) => parseUser(data))
}
const fetchUserRelationship = ({id, credentials}) => {
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url }, response))
}
return resolve(json)
}))
})
}
const fetchFriends = ({id, page, credentials}) => {
let url = `${FRIENDS_URL}?user_id=${id}`
if (page) {
@ -347,8 +362,8 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
dms: DM_TIMELINE_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL,
media: QVITTER_USER_TIMELINE_URL,
user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
tag: TAG_TIMELINE_URL
}
@ -357,15 +372,16 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
let url = timelineUrls[timeline]
if (timeline === 'user' || timeline === 'media') {
url = url(userId)
}
if (since) {
params.push(['since_id', since])
}
if (until) {
params.push(['max_id', until])
}
if (userId) {
params.push(['user_id', userId])
}
if (tag) {
url += `/${tag}.json`
}
@ -587,6 +603,7 @@ const apiService = {
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
favorite,
unfavorite,
retweet,

View file

@ -30,6 +30,10 @@ const backendInteractorService = (credentials) => {
return apiService.fetchUser({id, credentials})
}
const fetchUserRelationship = ({id}) => {
return apiService.fetchUserRelationship({id, credentials})
}
const followUser = (id) => {
return apiService.followUser({credentials, id})
}
@ -92,6 +96,7 @@ const backendInteractorService = (credentials) => {
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetching,

View file

@ -39,10 +39,10 @@ export const parseUser = (data) => {
return output
}
output.name = null // missing
// output.name = ??? missing
output.name_html = data.display_name
output.description = null // missing
// output.description = ??? missing
output.description_html = data.note
// Utilize avatar_static for gif avatars?
@ -59,10 +59,14 @@ export const parseUser = (data) => {
output.statusnet_profile_url = data.url
if (data.pleroma) {
const pleroma = data.pleroma
output.follows_you = pleroma.follows_you
output.statusnet_blocking = pleroma.statusnet_blocking
output.muted = pleroma.muted
const relationship = data.pleroma.relationship
if (relationship) {
output.follows_you = relationship.followed_by
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
}
}
// Missing, trying to recover
@ -83,7 +87,7 @@ export const parseUser = (data) => {
output.friends_count = data.friends_count
output.bot = null // missing
// output.bot = ??? missing
output.statusnet_profile_url = data.statusnet_profile_url
@ -134,7 +138,7 @@ const parseAttachment = (data) => {
output.meta = data.meta // not present in BE yet
} else {
output.mimetype = data.mimetype
output.meta = null // missing
// output.meta = ??? missing
}
output.url = data.url
@ -166,7 +170,7 @@ export const parseStatus = (data) => {
output.in_reply_to_user_id = data.in_reply_to_account_id
// Missing!! fix in UI?
output.in_reply_to_screen_name = null
// output.in_reply_to_screen_name = ???
// Not exactly the same but works
output.statusnet_conversation_id = data.id
@ -179,8 +183,7 @@ export const parseStatus = (data) => {
output.summary_html = data.spoiler_text
output.external_url = data.url
// FIXME missing!!
output.is_local = false
// output.is_local = ??? missing
} else {
output.favorited = data.favorited
output.fave_num = data.fave_num
@ -259,7 +262,7 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = null // missing
// output.seen = ??? missing
output.status = parseStatus(data.status)
output.action = output.status // not sure
output.from_profile = parseUser(data.account)

View file

@ -12,9 +12,13 @@ const mutations = {
setError: () => {}
}
const actions = {
fetchUser: () => {},
fetchUserByScreenName: () => {}
}
const testGetters = {
userByName: state => getters.userByName(state.users),
userById: state => getters.userById(state.users)
findUser: state => getters.findUser(state.users)
}
const localUser = {
@ -31,6 +35,7 @@ const extUser = {
const externalProfileStore = new Vuex.Store({
mutations,
actions,
getters: testGetters,
state: {
api: {
@ -89,7 +94,7 @@ const externalProfileStore = new Vuex.Store({
currentUser: {
credentials: ''
},
usersObject: [extUser],
usersObject: { 100: extUser },
users: [extUser]
}
}
@ -97,6 +102,7 @@ const externalProfileStore = new Vuex.Store({
const localProfileStore = new Vuex.Store({
mutations,
actions,
getters: testGetters,
state: {
api: {
@ -155,7 +161,7 @@ const localProfileStore = new Vuex.Store({
currentUser: {
credentials: ''
},
usersObject: [localUser],
usersObject: { 100: localUser, 'testuser': localUser },
users: [localUser]
}
}

View file

@ -34,40 +34,31 @@ describe('The users module', () => {
})
})
describe('getUserByName', () => {
describe('findUser', () => {
it('returns user with matching screen_name', () => {
const user = { screen_name: 'Guy', id: '1' }
const state = {
users: [
{ screen_name: 'Guy', id: '1' }
]
usersObject: {
1: user,
guy: user
}
}
const name = 'Guy'
const expected = { screen_name: 'Guy', id: '1' }
expect(getters.userByName(state)(name)).to.eql(expected)
expect(getters.findUser(state)(name)).to.eql(expected)
})
it('returns user with matching screen_name with different case', () => {
const state = {
users: [
{ screen_name: 'guy', id: '1' }
]
}
const name = 'Guy'
const expected = { screen_name: 'guy', id: '1' }
expect(getters.userByName(state)(name)).to.eql(expected)
})
})
describe('getUserById', () => {
it('returns user with matching id', () => {
const user = { screen_name: 'Guy', id: '1' }
const state = {
users: [
{ screen_name: 'Guy', id: '1' }
]
usersObject: {
1: user,
guy: user
}
}
const id = '1'
const expected = { screen_name: 'Guy', id: '1' }
expect(getters.userById(state)(id)).to.eql(expected)
expect(getters.findUser(state)(id)).to.eql(expected)
})
})
})