diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index a51f895e..0a4ec857 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -198,6 +198,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
+ store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
@@ -212,6 +213,16 @@ const getNodeInfo = async ({ store }) => {
}
const afterStoreSetup = async ({ store, i18n }) => {
+ if (store.state.config.customTheme) {
+ // This is a hack to deal with async loading of config.json and themes
+ // See: style_setter.js, setPreset()
+ window.themeLoaded = true
+ store.dispatch('setOption', {
+ name: 'customTheme',
+ value: store.state.config.customTheme
+ })
+ }
+
const apiConfig = await getStatusnetConfig({ store })
const staticConfig = await getStaticConfig()
await setSettings({ store, apiConfig, staticConfig })
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index 11fa27b4..c459ff1b 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -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
diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 5dd0a9e5..65c9cfb5 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -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
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 9e18fe15..c90da6d4 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -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 () {
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index e513b993..e6fed3b5 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -23,6 +23,11 @@ const UserAvatar = {
imageLoadError () {
this.showPlaceholder = true
}
+ },
+ watch: {
+ src () {
+ this.showPlaceholder = false
+ }
}
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 80d15a27..43a77f45 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -15,6 +15,9 @@ export default {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
+ created () {
+ this.$store.dispatch('fetchUserRelationship', this.user.id)
+ },
computed: {
classes () {
return [{
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 54126514..82df4510 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -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,9 @@ 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
+ // This needs fetchedUserId so that computed will be refreshed when user is fetched
+ return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
},
user () {
if (this.timeline.statuses[0]) {
@@ -93,9 +80,6 @@ const UserProfile = {
}
return {}
},
- fetchBy () {
- return this.isExternal ? this.userId : this.userName
- },
isExternal () {
return this.$route.name === 'external-user-profile'
},
@@ -109,14 +93,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 +136,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)()
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 7d4a8b1f..d449eb85 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -11,7 +11,7 @@
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
:timeline-name="'user'"
- :user-id="fetchBy"
+ :user-id="userId"
/>
@@ -25,7 +25,7 @@
:embedded="true" :title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
- :user-id="fetchBy"
+ :user-id="userId"
/>
{
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}
diff --git a/src/modules/users.js b/src/modules/users.js
index 26884750..1fe12fc8 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -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,17 @@ 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]
+ if (user) {
+ 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 +133,14 @@ 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 => {
+ const result = state.usersObject[query]
+ // In case it's a screen_name, we can try searching case-insensitive
+ if (!result && typeof query === 'string') {
+ return state.usersObject[query.toLowerCase()]
+ }
+ return result
+ }
}
export const defaultState = {
@@ -147,7 +160,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()
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 03553d75..fab48266 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -28,11 +28,9 @@ 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_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 SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+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: MASTODON_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`
}
@@ -588,6 +604,7 @@ const apiService = {
blockUser,
unblockUser,
fetchUser,
+ fetchUserRelationship,
favorite,
unfavorite,
retweet,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 7e972d7b..cbd0b733 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -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,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 874fb5de..c31496e8 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -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
+ }
}
// TODO: handle is_local
@@ -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
@@ -178,8 +182,6 @@ export const parseStatus = (data) => {
output.summary = data.spoiler_text
output.summary_html = data.spoiler_text
output.external_url = data.url
-
- // TODO: handle is_local
output.is_local = data.pleroma.local
} else {
output.favorited = data.favorited
@@ -271,6 +273,7 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
+
output.seen = null // missing
output.status = output.type === 'follow'
? parseFollow(data)
diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js
index 41fd9cd0..847481f3 100644
--- a/test/unit/specs/components/user_profile.spec.js
+++ b/test/unit/specs/components/user_profile.spec.js
@@ -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]
}
}
diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js
index 4d49ee24..c8bc0ae7 100644
--- a/test/unit/specs/modules/users.spec.js
+++ b/test/unit/specs/modules/users.spec.js
@@ -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)
})
})
})