diff --git a/src/App.scss b/src/App.scss
index 598735d9..244b3474 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -154,7 +154,7 @@ input, textarea, .select {
background: transparent;
border: none;
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, --text, $fallback--text);
margin: 0;
padding: 0 2em 0 .2em;
font-family: sans-serif;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index cd88c188..f5e84cbc 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -4,10 +4,11 @@ import routes from './routes'
import App from '../App.vue'
-const afterStoreSetup = ({ store, i18n }) => {
- window.fetch('/api/statusnet/config.json')
- .then((res) => res.json())
- .then((data) => {
+const getStatusnetConfig = async ({ store }) => {
+ try {
+ const res = await window.fetch('/api/statusnet/config.json')
+ if (res.ok) {
+ const data = await res.json()
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name })
@@ -28,138 +29,168 @@ const afterStoreSetup = ({ store, i18n }) => {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
}
- var apiConfig = data.site.pleromafe
+ return data.site.pleromafe
+ } else {
+ throw (res)
+ }
+ } catch (error) {
+ console.error('Could not load statusnet config, potentially fatal')
+ console.error(error)
+ }
+}
- window.fetch('/static/config.json')
- .then((res) => res.json())
- .catch((err) => {
- console.warn('Failed to load static/config.json, continuing without it.')
- console.warn(err)
- return {}
- })
- .then((staticConfig) => {
- const overrides = window.___pleromafe_dev_overrides || {}
- const env = window.___pleromafe_mode.NODE_ENV
+const getStaticConfig = async () => {
+ try {
+ const res = await window.fetch('/static/config.json')
+ if (res.ok) {
+ return res.json()
+ } else {
+ throw (res)
+ }
+ } catch (error) {
+ console.warn('Failed to load static/config.json, continuing without it.')
+ console.warn(error)
+ return {}
+ }
+}
- // This takes static config and overrides properties that are present in apiConfig
- let config = {}
- if (overrides.staticConfigPreference && env === 'development') {
- console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
- config = Object.assign({}, apiConfig, staticConfig)
- } else {
- config = Object.assign({}, staticConfig, apiConfig)
- }
+const setSettings = async ({ apiConfig, staticConfig, store }) => {
+ const overrides = window.___pleromafe_dev_overrides || {}
+ const env = window.___pleromafe_mode.NODE_ENV
- const copyInstanceOption = (name) => {
- store.dispatch('setInstanceOption', {name, value: config[name]})
- }
+ // This takes static config and overrides properties that are present in apiConfig
+ let config = {}
+ if (overrides.staticConfigPreference && env === 'development') {
+ console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
+ config = Object.assign({}, apiConfig, staticConfig)
+ } else {
+ config = Object.assign({}, staticConfig, apiConfig)
+ }
- copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('background')
- copyInstanceOption('hidePostStats')
- copyInstanceOption('hideUserStats')
- copyInstanceOption('hideFilteredStatuses')
- copyInstanceOption('logo')
+ const copyInstanceOption = (name) => {
+ store.dispatch('setInstanceOption', { name, value: config[name] })
+ }
- store.dispatch('setInstanceOption', {
- name: 'logoMask',
- value: typeof config.logoMask === 'undefined'
- ? true
- : config.logoMask
- })
+ copyInstanceOption('nsfwCensorImage')
+ copyInstanceOption('background')
+ copyInstanceOption('hidePostStats')
+ copyInstanceOption('hideUserStats')
+ copyInstanceOption('hideFilteredStatuses')
+ copyInstanceOption('logo')
- store.dispatch('setInstanceOption', {
- name: 'logoMargin',
- value: typeof config.logoMargin === 'undefined'
- ? 0
- : config.logoMargin
- })
+ store.dispatch('setInstanceOption', {
+ name: 'logoMask',
+ value: typeof config.logoMask === 'undefined'
+ ? true
+ : config.logoMask
+ })
- copyInstanceOption('redirectRootNoLogin')
- copyInstanceOption('redirectRootLogin')
- copyInstanceOption('showInstanceSpecificPanel')
- copyInstanceOption('scopeOptionsEnabled')
- copyInstanceOption('formattingOptionsEnabled')
- copyInstanceOption('collapseMessageWithSubject')
- copyInstanceOption('loginMethod')
- copyInstanceOption('scopeCopy')
- copyInstanceOption('subjectLineBehavior')
- copyInstanceOption('postContentType')
- copyInstanceOption('alwaysShowSubjectInput')
- copyInstanceOption('noAttachmentLinks')
- copyInstanceOption('showFeaturesPanel')
+ store.dispatch('setInstanceOption', {
+ name: 'logoMargin',
+ value: typeof config.logoMargin === 'undefined'
+ ? 0
+ : config.logoMargin
+ })
- if (config.chatDisabled) {
- store.dispatch('disableChat')
- }
+ copyInstanceOption('redirectRootNoLogin')
+ copyInstanceOption('redirectRootLogin')
+ copyInstanceOption('showInstanceSpecificPanel')
+ copyInstanceOption('scopeOptionsEnabled')
+ copyInstanceOption('formattingOptionsEnabled')
+ copyInstanceOption('hideMutedPosts')
+ copyInstanceOption('collapseMessageWithSubject')
+ copyInstanceOption('loginMethod')
+ copyInstanceOption('scopeCopy')
+ copyInstanceOption('subjectLineBehavior')
+ copyInstanceOption('postContentType')
+ copyInstanceOption('alwaysShowSubjectInput')
+ copyInstanceOption('noAttachmentLinks')
+ copyInstanceOption('showFeaturesPanel')
- return store.dispatch('setTheme', config['theme'])
- })
- .then(() => {
- const router = new VueRouter({
- mode: 'history',
- routes: routes(store),
- scrollBehavior: (to, _from, savedPosition) => {
- if (to.matched.some(m => m.meta.dontScroll)) {
- return false
- }
- return savedPosition || { x: 0, y: 0 }
- }
- })
+ if ((config.chatDisabled)) {
+ store.dispatch('disableChat')
+ } else {
+ store.dispatch('initializeSocket')
+ }
- /* eslint-disable no-new */
- new Vue({
- router,
- store,
- i18n,
- el: '#app',
- render: h => h(App)
- })
- })
- })
+ return store.dispatch('setTheme', config['theme'])
+}
- window.fetch('/static/terms-of-service.html')
- .then((res) => res.text())
- .then((html) => {
+const getTOS = async ({ store }) => {
+ try {
+ const res = await window.fetch('/static/terms-of-service.html')
+ if (res.ok) {
+ const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html })
- })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn("Can't load TOS")
+ console.warn(e)
+ }
+}
- window.fetch('/api/pleroma/emoji.json')
- .then(
- (res) => res.json()
- .then(
- (values) => {
- const emoji = Object.keys(values).map((key) => {
- return { shortcode: key, image_url: values[key] }
- })
- store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
- store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
- },
- (failure) => {
- store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
- }
- ),
- (error) => console.log(error)
- )
+const getInstancePanel = async ({ store }) => {
+ try {
+ const res = await window.fetch('/instance/panel.html')
+ if (res.ok) {
+ const html = await res.text()
+ store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn("Can't load instance panel")
+ console.warn(e)
+ }
+}
- window.fetch('/static/emoji.json')
- .then((res) => res.json())
- .then((values) => {
+const getStaticEmoji = async ({ store }) => {
+ try {
+ const res = await window.fetch('/static/emoji.json')
+ if (res.ok) {
+ const values = await res.json()
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
- })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn("Can't load static emoji")
+ console.warn(e)
+ }
+}
- window.fetch('/instance/panel.html')
- .then((res) => res.text())
- .then((html) => {
- store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
- })
+// This is also used to indicate if we have a 'pleroma backend' or not.
+// Somewhat weird, should probably be somewhere else.
+const getCustomEmoji = async ({ store }) => {
+ try {
+ const res = await window.fetch('/api/pleroma/emoji.json')
+ if (res.ok) {
+ const values = await res.json()
+ const emoji = Object.keys(values).map((key) => {
+ return { shortcode: key, image_url: values[key] }
+ })
+ store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
+ store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
+ console.warn("Can't load custom emojis, maybe not a Pleroma instance?")
+ console.warn(e)
+ }
+}
- window.fetch('/nodeinfo/2.0.json')
- .then((res) => res.json())
- .then((data) => {
+const getNodeInfo = async ({ store }) => {
+ try {
+ const res = await window.fetch('/nodeinfo/2.0.json')
+ if (res.ok) {
+ const data = await res.json()
const metadata = data.metadata
const features = metadata.features
@@ -167,14 +198,71 @@ const afterStoreSetup = ({ store, i18n }) => {
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
- store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
-
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 })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
+
+ const software = data.software
+ store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
+
+ const frontendVersion = window.___pleromafe_commit_hash
+ store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn('Could not load nodeinfo')
+ console.warn(e)
+ }
+}
+
+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 })
+ await getTOS({ store })
+ await getInstancePanel({ store })
+ await getStaticEmoji({ store })
+ await getCustomEmoji({ store })
+ await getNodeInfo({ store })
+
+ // Now we have the server settings and can try logging in
+ if (store.state.oauth.token) {
+ store.dispatch('loginUser', store.state.oauth.token)
+ }
+
+ const router = new VueRouter({
+ mode: 'history',
+ routes: routes(store),
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (to.matched.some(m => m.meta.dontScroll)) {
+ return false
+ }
+ return savedPosition || { x: 0, y: 0 }
+ }
+ })
+
+ /* eslint-disable no-new */
+ return new Vue({
+ router,
+ store,
+ i18n,
+ el: '#app',
+ render: h => h(App)
+ })
}
export default afterStoreSetup
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 9b80c72b..8afe8b44 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -8,8 +8,8 @@
-
- {{ user.name }}
+
+ {{ user.name }}
@@ -52,6 +52,14 @@
width: 16px;
vertical-align: middle;
}
+
+ &-value {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
}
&-expanded-content {
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/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index 425c9c3e..ac4e265a 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -1,4 +1,5 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import RemoteFollow from '../remote_follow/remote_follow.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const FollowCard = {
@@ -14,13 +15,17 @@ const FollowCard = {
}
},
components: {
- BasicUserCard
+ BasicUserCard,
+ RemoteFollow
},
computed: {
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.following || this.updated && !this.updated.following
+ },
+ loggedIn () {
+ return this.$store.state.users.currentUser
}
},
methods: {
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 6cb064eb..9f314fd3 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -4,9 +4,12 @@
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
+
+
+
-
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 8e489704..550fe76f 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..197c61d5 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,4 +1,5 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
+import RemoteFollow from '../remote_follow/remote_follow.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -15,6 +16,9 @@ export default {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
+ created () {
+ this.$store.dispatch('fetchUserRelationship', this.user.id)
+ },
computed: {
classes () {
return [{
@@ -96,7 +100,8 @@ export default {
}
},
components: {
- UserAvatar
+ UserAvatar,
+ RemoteFollow
},
methods: {
followUser () {
@@ -116,24 +121,16 @@ export default {
})
},
blockUser () {
- const store = this.$store
- store.state.api.backendInteractor.blockUser(this.user.id)
- .then((blockedUser) => {
- store.commit('addNewUsers', [blockedUser])
- store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
- store.commit('removeStatus', { timeline: 'public', userId: this.user.id })
- store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id })
- })
+ this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
- const store = this.$store
- store.state.api.backendInteractor.unblockUser(this.user.id)
- .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
+ this.$store.dispatch('unblockUser', this.user.id)
},
- toggleMute () {
- const store = this.$store
- store.commit('setMuted', {user: this.user, muted: !this.user.muted})
- store.state.api.backendInteractor.setUserMute(this.user)
+ muteUser () {
+ this.$store.dispatch('muteUser', this.user.id)
+ },
+ unmuteUser () {
+ this.$store.dispatch('unmuteUser', this.user.id)
},
setProfileView (v) {
if (this.switcher) {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 690e1bde..3259d1c5 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -74,24 +74,18 @@
-
+
{{ $t('user_card.muted') }}
-
+
{{ $t('user_card.mute') }}
-
-
+
+
@@ -375,11 +369,6 @@
min-height: 28px;
}
- .remote-follow {
- max-width: 220px;
- min-height: 28px;
- }
-
.follow {
max-width: 220px;
min-height: 28px;
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 (!user.error) {
this.$store.commit('addNewUsers', [user])
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index a1123638..c9e68808 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -192,6 +192,12 @@
{{$t('settings.no_blocks')}}
+
+
+
+ {{$t('settings.no_mutes')}}
+
+
diff --git a/src/i18n/ar.json b/src/i18n/ar.json
index 242dab78..72e3010f 100644
--- a/src/i18n/ar.json
+++ b/src/i18n/ar.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "مقفل",
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
"content_type": {
- "plain_text": "نص صافٍ"
+ "text/plain": "نص صافٍ"
},
"content_warning": "الموضوع (اختياري)",
"default": "وصلت للتوّ إلى لوس أنجلس.",
diff --git a/src/i18n/ca.json b/src/i18n/ca.json
index d2f285df..8fa3a88b 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "bloquejat",
"attachments_sensitive": "Marca l'adjunt com a delicat",
"content_type": {
- "plain_text": "Text pla"
+ "text/plain": "Text pla"
},
"content_warning": "Assumpte (opcional)",
"default": "Em sento…",
diff --git a/src/i18n/cs.json b/src/i18n/cs.json
index 51e9d342..020092a6 100644
--- a/src/i18n/cs.json
+++ b/src/i18n/cs.json
@@ -71,7 +71,7 @@
"account_not_locked_warning_link": "uzamčen",
"attachments_sensitive": "Označovat přílohy jako citlivé",
"content_type": {
- "plain_text": "Prostý text",
+ "text/plain": "Prostý text",
"text/html": "HTML",
"text/markdown": "Markdown"
},
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 07d44348..fa9db16c 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -55,7 +55,7 @@
"account_not_locked_warning_link": "gesperrt",
"attachments_sensitive": "Anhänge als heikel markieren",
"content_type": {
- "plain_text": "Nur Text"
+ "text/plain": "Nur Text"
},
"content_warning": "Betreff (optional)",
"default": "Sitze gerade im Hofbräuhaus.",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 01fe2fba..ecf9628f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -25,6 +25,7 @@
"image_cropper": {
"crop_picture": "Crop picture",
"save": "Save",
+ "save_without_cropping": "Save without cropping",
"cancel": "Cancel"
},
"login": {
@@ -152,6 +153,7 @@
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
+ "hide_muted_posts": "Hide posts of muted users",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images",
@@ -347,6 +349,11 @@
"checkbox": "I have skimmed over terms and conditions",
"link": "a nice lil' link"
}
+ },
+ "version": {
+ "title": "Version",
+ "backend_version": "Backend Version",
+ "frontend_version": "Frontend Version"
}
},
"timeline": {
diff --git a/src/i18n/eo.json b/src/i18n/eo.json
index 34851a44..6c5b3a74 100644
--- a/src/i18n/eo.json
+++ b/src/i18n/eo.json
@@ -71,7 +71,7 @@
"account_not_locked_warning_link": "ŝlosita",
"attachments_sensitive": "Marki kunsendaĵojn kiel konsternajn",
"content_type": {
- "plain_text": "Plata teksto"
+ "text/plain": "Plata teksto"
},
"content_warning": "Temo (malnepra)",
"default": "Ĵus alvenis al la Universala Kongreso!",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index fe96dd08..a692eef9 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -61,7 +61,7 @@
"account_not_locked_warning_link": "bloqueada",
"attachments_sensitive": "Contenido sensible",
"content_type": {
- "plain_text": "Texto Plano"
+ "text/plain": "Texto Plano"
},
"content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.",
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 4f0ffb4b..fbe676cf 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -60,7 +60,7 @@
"account_not_locked_warning_link": "lukittu",
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
"content_type": {
- "plain_text": "Tavallinen teksti"
+ "text/plain": "Tavallinen teksti"
},
"content_warning": "Aihe (valinnainen)",
"default": "Tulin juuri saunasta.",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 1209556a..8f9f243e 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -51,7 +51,7 @@
"account_not_locked_warning_link": "verrouillé",
"attachments_sensitive": "Marquer le média comme sensible",
"content_type": {
- "plain_text": "Texte brut"
+ "text/plain": "Texte brut"
},
"content_warning": "Sujet (optionnel)",
"default": "Écrivez ici votre prochain statut.",
diff --git a/src/i18n/ga.json b/src/i18n/ga.json
index 5be9297a..31250876 100644
--- a/src/i18n/ga.json
+++ b/src/i18n/ga.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "faoi glas",
"attachments_sensitive": "Marcáil ceangaltán mar íogair",
"content_type": {
- "plain_text": "Gnáth-théacs"
+ "text/plain": "Gnáth-théacs"
},
"content_warning": "Teideal (roghnach)",
"default": "Lá iontach anseo i nGaillimh",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index 213e6170..ea581e05 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "נעול",
"attachments_sensitive": "סמן מסמכים מצורפים כלא בטוחים לצפייה",
"content_type": {
- "plain_text": "טקסט פשוט"
+ "text/plain": "טקסט פשוט"
},
"content_warning": "נושא (נתון לבחירה)",
"default": "הרגע נחת ב-ל.א.",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 385d21aa..f441292e 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -175,7 +175,7 @@
"account_not_locked_warning_link": "bloccato",
"attachments_sensitive": "Segna allegati come sensibili",
"content_type": {
- "plain_text": "Testo normale"
+ "text/plain": "Testo normale"
},
"content_warning": "Oggetto (facoltativo)",
"default": "Appena atterrato in L.A.",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index f39a5a7c..b77f5531 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -61,7 +61,7 @@
"account_not_locked_warning_link": "ロックされたアカウント",
"attachments_sensitive": "ファイルをNSFWにする",
"content_type": {
- "plain_text": "プレーンテキスト"
+ "text/plain": "プレーンテキスト"
},
"content_warning": "せつめい (かかなくてもよい)",
"default": "はねだくうこうに、つきました。",
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index 336e464f..402a354c 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -56,7 +56,7 @@
"account_not_locked_warning_link": "잠김",
"attachments_sensitive": "첨부물을 민감함으로 설정",
"content_type": {
- "plain_text": "평문"
+ "text/plain": "평문"
},
"content_warning": "주제 (필수 아님)",
"default": "LA에 도착!",
diff --git a/src/i18n/nb.json b/src/i18n/nb.json
index 39e054f7..298dc0b9 100644
--- a/src/i18n/nb.json
+++ b/src/i18n/nb.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "låst",
"attachments_sensitive": "Merk vedlegg som sensitive",
"content_type": {
- "plain_text": "Klar tekst"
+ "text/plain": "Klar tekst"
},
"content_warning": "Tema (valgfritt)",
"default": "Landet akkurat i L.A.",
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index 799e22b9..7e2f0604 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -57,7 +57,7 @@
"account_not_locked_warning_link": "gesloten",
"attachments_sensitive": "Markeer bijlage als gevoelig",
"content_type": {
- "plain_text": "Gewone tekst"
+ "text/plain": "Gewone tekst"
},
"content_warning": "Onderwerp (optioneel)",
"default": "Tijd voor een pauze!",
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index fd5ccc97..ecc4df61 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -59,19 +59,21 @@
"broken_favorite": "Estatut desconegut, sèm a lo cercar...",
"favorited_you": "a aimat vòstre estatut",
"followed_you": "vos a seguit",
- "load_older": "Cargar las notificaciones mai ancianas",
+ "load_older": "Cargar las notificacions mai ancianas",
"notifications": "Notficacions",
- "read": "Legit !",
+ "read": "Legit !",
"repeated_you": "a repetit vòstre estatut",
"no_more_notifications": "Pas mai de notificacions"
},
"post_status": {
"new_status": "Publicar d’estatuts novèls",
- "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu'a vòstres seguidors.",
+ "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu’a vòstres seguidors.",
"account_not_locked_warning_link": "clavat",
"attachments_sensitive": "Marcar las pèças juntas coma sensiblas",
"content_type": {
- "plain_text": "Tèxte brut"
+ "text/plain": "Tèxte brut",
+ "text/html": "HTML",
+ "text/markdown": "Markdown"
},
"content_warning": "Avís de contengut (opcional)",
"default": "Escrivètz aquí vòstre estatut.",
@@ -118,12 +120,12 @@
"blocks_tab": "Blocatges",
"btnRadius": "Botons",
"cBlue": "Blau (Respondre, seguir)",
- "cGreen": "Verd (Repartajar)",
+ "cGreen": "Verd (Repertir)",
"cOrange": "Irange (Aimar)",
"cRed": "Roge (Anullar)",
"change_password": "Cambiar lo senhal",
"change_password_error": "Una error s’es producha en cambiant lo senhal.",
- "changed_password": "Senhal corrèctament cambiat !",
+ "changed_password": "Senhal corrèctament cambiat !",
"collapse_subject": "Replegar las publicacions amb de subjèctes",
"composing": "Escritura",
"confirm_new_password": "Confirmatz lo nòu senhal",
@@ -134,7 +136,7 @@
"default_vis": "Nivèl de visibilitat per defaut",
"delete_account": "Suprimir lo compte",
"delete_account_description": "Suprimir vòstre compte e los messatges per sempre.",
- "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instància.",
+ "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrator d’instància.",
"delete_account_instructions": "Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.",
"avatar_size_instruction": "La talha minimum recomandada pels imatges d’avatar es 150x150 pixèls.",
"export_theme": "Enregistrar la preconfiguracion",
@@ -154,14 +156,14 @@
"hide_isp": "Amagar lo panèl especial instància",
"preload_images": "Precargar los imatges",
"use_one_click_nsfw": "Dobrir las pèças juntas NSFW amb un clic",
- "hide_post_stats": "Amagar los estatistics de publicacion (ex. lo ombre de favorits)",
+ "hide_post_stats": "Amagar las estatisticas de publicacion (ex. lo nombre de favorits)",
"hide_user_stats": "Amagar las estatisticas de l’utilizaire (ex. lo nombre de seguidors)",
"hide_filtered_statuses": "Amagar los estatuts filtrats",
"import_followers_from_a_csv_file": "Importar los seguidors d’un fichièr csv",
"import_theme": "Cargar un tèma",
"inputRadius": "Camps tèxte",
"checkboxRadius": "Casas de marcar",
- "instance_default": "(defaut : {value})",
+ "instance_default": "(defaut : {value})",
"instance_default_simple": "(defaut)",
"interface": "Interfàcia",
"interfaceLanguage": "Lenga de l’interfàcia",
@@ -172,7 +174,7 @@
"loop_video": "Bocla vidèo",
"loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)",
"mutes_tab": "Agamats",
- "play_videos_in_modal": "Legir las vidèoas dirèctament dins la visualizaira mèdia",
+ "play_videos_in_modal": "Legir las vidèos dirèctament dins la visualizaira mèdia",
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
"name": "Nom",
"name_bio": "Nom & Bio",
@@ -223,7 +225,7 @@
"post_status_content_type": "Publicar lo tipe de contengut dels estatuts",
"stop_gifs": "Lançar los GIFs al subrevòl",
"streaming": "Activar lo cargament automatic dels novèls estatus en anar amont",
- "text": "Tèxt",
+ "text": "Tèxte",
"theme": "Tèma",
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
@@ -234,6 +236,117 @@
"values": {
"false": "non",
"true": "òc"
+ },
+ "notifications": "Notificacions",
+ "enable_web_push_notifications": "Activar las notificacions web push",
+ "style": {
+ "switcher": {
+ "keep_color": "Gardar las colors",
+ "keep_shadows": "Gardar las ombras",
+ "keep_opacity": "Gardar l’opacitat",
+ "keep_roundness": "Gardar la redondetat",
+ "keep_fonts": "Gardar las polissas",
+ "save_load_hint": "Las opcions « Gardar » permeton de servar las opcions configuradas actualament quand seleccionatz o cargatz un tèma, permeton tanben d’enregistrar aquelas opcions quand exportatz un tèma. Quand totas las casas son pas marcadas, l’exportacion de tèma o enregistrarà tot.",
+ "reset": "Restablir",
+ "clear_all": "O escafar tot",
+ "clear_opacity": "Escafar l’opacitat"
+ },
+ "common": {
+ "color": "Color",
+ "opacity": "Opacitat",
+ "contrast": {
+ "hint": "Lo coeficient de contraste es de {ratio}. Dòna {level} {context}",
+ "level": {
+ "aa": "un nivèl AA minimum recomandat",
+ "aaa": "un nivèl AAA recomandat",
+ "bad": "pas un nivèl d’accessibilitat recomandat"
+ },
+ "context": {
+ "18pt": "pel tèxte grand (18pt+)",
+ "text": "pel tèxte"
+ }
+ }
+ },
+ "common_colors": {
+ "_tab_label": "Comun",
+ "main": "Colors comunas",
+ "foreground_hint": "Vejatz « Avançat » per mai de paramètres detalhats",
+ "rgbo": "Icònas, accents, badges"
+ },
+ "advanced_colors": {
+ "_tab_label": "Avançat",
+ "alert": "Rèire plan d’alèrtas",
+ "alert_error": "Error",
+ "badge": "Rèire plan dels badges",
+ "badge_notification": "Notificacion",
+ "panel_header": "Bandièra del tablèu de bòrd",
+ "top_bar": "Barra amont",
+ "borders": "Caires",
+ "buttons": "Botons",
+ "inputs": "Camps tèxte",
+ "faint_text": "Tèxte descolorit"
+ },
+ "radii": {
+ "_tab_label": "Redondetat"
+ },
+ "shadows": {
+ "_tab_label": "Ombra e luminositat",
+ "component": "Compausant",
+ "override": "Subrecargar",
+ "shadow_id": "Ombra #{value}",
+ "blur": "Fosc",
+ "spread": "Espandiment",
+ "inset": "Incrustacion",
+ "hint": "Per las ombras podètz tanben utilizar --variable coma valor de color per emplegar una variable CSS3. Notatz que lo paramètre d’opacitat foncionarà pas dins aquel cas.",
+ "filter_hint": {
+ "always_drop_shadow": "Avertiment, aquel ombra utiliza totjorn {0} quand lo navigator es compatible.",
+ "drop_shadow_syntax": "{0} es pas compatible amb lo paramètre {1} e lo mot clau {2}.",
+ "avatar_inset": "Notatz que combinar d’ombras incrustadas e pas incrustadas pòt donar de resultats inesperats amb los avatars transparents.",
+ "spread_zero": "L’ombra amb un espandiment de > 0 apareisserà coma reglat a zèro",
+ "inset_classic": "L’ombra d’incrustacion utilizarà {0}"
+ },
+ "components": {
+ "panel": "Tablèu",
+ "panelHeader": "Bandièra del tablèu",
+ "topBar": "Barra amont",
+ "avatar": "Utilizar l’avatar (vista perfil)",
+ "avatarStatus": "Avatar de l’utilizaire (afichatge publicacion)",
+ "popup": "Fenèstras sorgissentas e astúcias",
+ "button": "Boton",
+ "buttonHover": "Boton (en passar la mirga)",
+ "buttonPressed": "Boton (en quichar)",
+ "buttonPressedHover": "Boton (en quichar e passar)",
+ "input": "Camp tèxte"
+ }
+ },
+ "fonts": {
+ "_tab_label": "Polissas",
+ "help": "Selecionatz la polissa d’utilizar pels elements de l’UI. Per « Personalizada » vos cal picar lo nom exacte tal coma apareis sul sistèma.",
+ "components": {
+ "interface": "Interfàcia",
+ "input": "Camps tèxte",
+ "post": "Tèxte de publicacion",
+ "postCode": "Tèxte Monospaced dins las publicacion (tèxte formatat)"
+ },
+ "family": "Nom de la polissa",
+ "size": "Talha (en px)",
+ "weight": "Largor (gras)",
+ "custom": "Personalizada"
+ },
+ "preview": {
+ "header": "Apercebut",
+ "content": "Contengut",
+ "error": "Error d’exemple",
+ "button": "Boton",
+ "text": "A tròç de mai de {0} e {1}",
+ "mono": "contengut",
+ "input": "arribada al país.",
+ "faint_link": "manual d’ajuda",
+ "fine_print": "Legissètz nòstre {0} per legir pas res d’util !",
+ "header_faint": "Va plan",
+ "checkbox": "Ai legit los tèrmes e condicions d’utilizacion",
+ "link": "un pichon ligam simpatic"
+ }
}
},
"timeline": {
@@ -241,19 +354,21 @@
"conversation": "Conversacion",
"error_fetching": "Error en cercant de mesas a jorn",
"load_older": "Ne veire mai",
+ "no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir",
"repeated": "repetit",
"show_new": "Ne veire mai",
"up_to_date": "A jorn",
- "no_retweet_hint": "La publicacion marcada coma pels seguidors solament o dirècte pòt pas èsser repetida"
+ "no_more_statuses": "Pas mai d’estatuts",
+ "no_statuses": "Cap d’estatuts"
},
"status": {
- "reply_to": "Respondre à",
+ "reply_to": "Respond a",
"replies_list": "Responsas :"
},
"user_card": {
"approve": "Validar",
"block": "Blocar",
- "blocked": "Blocat !",
+ "blocked": "Blocat !",
"deny": "Refusar",
"favorites": "Favorits",
"follow": "Seguir",
@@ -263,8 +378,8 @@
"follow_unfollow": "Quitar de seguir",
"followees": "Abonaments",
"followers": "Seguidors",
- "following": "Seguit !",
- "follows_you": "Vos sèc !",
+ "following": "Seguit !",
+ "follows_you": "Vos sèc !",
"its_you": "Sètz vos !",
"media": "Mèdia",
"mute": "Amagar",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index cbc2c9a3..41a34483 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -51,7 +51,7 @@
"public_tl": "Linha do tempo pública",
"timeline": "Linha do tempo",
"twkn": "Toda a rede conhecida",
- "user_search": "Busca de usuário",
+ "user_search": "Buscar usuários",
"who_to_follow": "Quem seguir",
"preferences": "Preferências"
},
@@ -67,11 +67,11 @@
},
"post_status": {
"new_status": "Postar novo status",
- "account_not_locked_warning": "Sua conta não está {0}. Qualquer pessoa pode te seguir para ver seus posts restritos.",
- "account_not_locked_warning_link": "fechada",
+ "account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).",
+ "account_not_locked_warning_link": "restrita",
"attachments_sensitive": "Marcar anexos como sensíveis",
"content_type": {
- "plain_text": "Texto puro"
+ "text/plain": "Texto puro"
},
"content_warning": "Assunto (opcional)",
"default": "Acabei de chegar no Rio!",
@@ -115,7 +115,7 @@
"avatarRadius": "Avatares",
"background": "Pano de Fundo",
"bio": "Biografia",
- "blocks_tab": "Blocos",
+ "blocks_tab": "Bloqueios",
"btnRadius": "Botões",
"cBlue": "Azul (Responder, seguir)",
"cGreen": "Verde (Repetir)",
@@ -125,7 +125,7 @@
"change_password_error": "Houve um erro ao modificar sua senha.",
"changed_password": "Senha modificada com sucesso!",
"collapse_subject": "Esconder posts com assunto",
- "composing": "Escrevendo",
+ "composing": "Escrita",
"confirm_new_password": "Confirmar nova senha",
"current_avatar": "Seu avatar atual",
"current_password": "Sua senha atual",
@@ -139,7 +139,7 @@
"avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.",
"export_theme": "Salvar predefinições",
"filtering": "Filtragem",
- "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas, uma por linha.",
+ "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.",
"follow_export": "Exportar quem você segue",
"follow_export_button": "Exportar quem você segue para um arquivo CSV",
"follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo",
@@ -178,7 +178,7 @@
"name_bio": "Nome & Biografia",
"new_password": "Nova senha",
"notification_visibility": "Tipos de notificação para mostrar",
- "notification_visibility_follows": "Seguidos",
+ "notification_visibility_follows": "Seguidas",
"notification_visibility_likes": "Favoritos",
"notification_visibility_mentions": "Menções",
"notification_visibility_repeats": "Repetições",
@@ -187,7 +187,7 @@
"no_mutes": "Sem silenciados",
"hide_follows_description": "Não mostrar quem estou seguindo",
"hide_followers_description": "Não mostrar quem me segue",
- "show_admin_badge": "Mostrar distintivo de Administrador em meu perfil",
+ "show_admin_badge": "Mostrar título de Administrador em meu perfil",
"show_moderator_badge": "Mostrar título de Moderador em meu perfil",
"nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis",
"oauth_tokens": "Token OAuth",
@@ -201,9 +201,9 @@
"profile_background": "Pano de fundo de perfil",
"profile_banner": "Capa de perfil",
"profile_tab": "Perfil",
- "radii_help": "Arredondar arestas da interface (em píxeis)",
+ "radii_help": "Arredondar arestas da interface (em pixel)",
"replies_in_timeline": "Respostas na linha do tempo",
- "reply_link_preview": "Habilitar a pré-visualização de link de respostas ao passar o mouse.",
+ "reply_link_preview": "Habilitar a pré-visualização de de respostas ao passar o mouse.",
"reply_visibility_all": "Mostrar todas as respostas",
"reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo",
"reply_visibility_self": "Só mostrar respostas direcionadas a mim",
@@ -212,7 +212,7 @@
"security_tab": "Segurança",
"scope_copy": "Copiar opções de privacidade ao responder (Mensagens diretas sempre copiam)",
"set_new_avatar": "Alterar avatar",
- "set_new_profile_background": "Alterar o plano de fundo de perfil",
+ "set_new_profile_background": "Alterar o pano de fundo de perfil",
"set_new_profile_banner": "Alterar capa de perfil",
"settings": "Configurações",
"subject_input_always_show": "Sempre mostrar campo de assunto",
@@ -220,9 +220,9 @@
"subject_line_email": "Como em email: \"re: assunto\"",
"subject_line_mastodon": "Como o Mastodon: copiar como está",
"subject_line_noop": "Não copiar",
- "post_status_content_type": "Postar tipo de conteúdo do status",
- "stop_gifs": "Reproduzir GIFs ao passar o cursor em cima",
- "streaming": "Habilitar o fluxo automático de postagens quando ao topo da página",
+ "post_status_content_type": "Tipo de conteúdo do status",
+ "stop_gifs": "Reproduzir GIFs ao passar o cursor",
+ "streaming": "Habilitar o fluxo automático de postagens no topo da página",
"text": "Texto",
"theme": "Tema",
"theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.",
@@ -235,7 +235,7 @@
"false": "não",
"true": "sim"
},
- "notifications": "Notifications",
+ "notifications": "Notificações",
"enable_web_push_notifications": "Habilitar notificações web push",
"style": {
"switcher": {
@@ -245,7 +245,7 @@
"keep_roundness": "Manter arredondado",
"keep_fonts": "Manter fontes",
"save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.",
- "reset": "Voltar ao padrão",
+ "reset": "Restaurar o padrão",
"clear_all": "Limpar tudo",
"clear_opacity": "Limpar opacidade"
},
@@ -319,7 +319,7 @@
},
"fonts": {
"_tab_label": "Fontes",
- "help": "Selecionar fonte dos elementos da interface. Para fonte \"personalizada\" você deve entrar exatamente o nome da fonte no sistema.",
+ "help": "Selecione as fontes dos elementos da interface. Para fonte \"personalizada\" você deve inserir o mesmo nome da fonte no sistema.",
"components": {
"interface": "Interface",
"input": "Campo de entrada",
@@ -383,7 +383,7 @@
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por dia",
- "remote_follow": "Seguidor Remoto",
+ "remote_follow": "Seguir remotamente",
"statuses": "Postagens",
"unblock": "Desbloquear",
"unblock_progress": "Desbloqueando...",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 089a98e2..da6dae5f 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -49,7 +49,7 @@
"account_not_locked_warning_link": "上锁",
"attachments_sensitive": "标记附件为敏感内容",
"content_type": {
- "plain_text": "纯文本"
+ "text/plain": "纯文本"
},
"content_warning": "主题(可选)",
"default": "刚刚抵达上海",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index e828a74b..7ab89c12 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -60,18 +60,6 @@ export default function createPersistedState ({
merge({}, store.state, savedState)
)
}
- 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
- })
- }
- if (store.state.oauth.token) {
- store.dispatch('loginUser', store.state.oauth.token)
- }
loaded = true
} catch (e) {
console.log("Couldn't load state")
diff --git a/src/main.js b/src/main.js
index a3265e3a..9ffc3727 100644
--- a/src/main.js
+++ b/src/main.js
@@ -53,9 +53,10 @@ const persistedStateOptions = {
'users.lastLoginName',
'oauth'
]
-}
+};
-createPersistedState(persistedStateOptions).then((persistedState) => {
+(async () => {
+ const persistedState = await createPersistedState(persistedStateOptions)
const store = new Vuex.Store({
modules: {
interface: interfaceModule,
@@ -75,7 +76,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
})
afterStoreSetup({ store, i18n })
-})
+})()
// These are inlined by webpack's DefinePlugin
/* eslint-disable */
diff --git a/src/modules/config.js b/src/modules/config.js
index 1c30c203..c5491c01 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -5,6 +5,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
const defaultState = {
colors: {},
+ hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
hideAttachments: false,
hideAttachmentsInConv: false,
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 24c52f9c..f778ac4d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -18,6 +18,7 @@ const defaultState = {
scopeOptionsEnabled: true,
formattingOptionsEnabled: false,
alwaysShowSubjectInput: true,
+ hideMutedPosts: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
@@ -48,7 +49,11 @@ const defaultState = {
// Html stuff
instanceSpecificPanelContent: '',
- tos: ''
+ tos: '',
+
+ // Version Information
+ backendVersion: '',
+ frontendVersion: ''
}
const instance = {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 6b512fa3..f14b8703 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -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}
diff --git a/src/modules/users.js b/src/modules/users.js
index 26884750..f83ae298 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,10 +91,31 @@ export const mutations = {
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
- saveBlocks (state, blockIds) {
+ 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
+ }
+ })
+ },
+ updateBlocks (state, blockedUsers) {
+ // Reset statusnet_blocking of all fetched users
+ each(state.users, (user) => { user.statusnet_blocking = false })
+ each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
+ },
+ saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
- saveMutes (state, muteIds) {
+ updateMutes (state, mutedUsers) {
+ // Reset muted of all fetched users
+ each(state.users, (user) => { user.muted = false })
+ each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
+ },
+ saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds
},
setUserForStatus (state, status) {
@@ -122,12 +143,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,39 +170,51 @@ 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()
.then((blocks) => {
- store.commit('saveBlocks', map(blocks, 'id'))
- store.commit('addNewUsers', blocks)
+ store.commit('saveBlockIds', map(blocks, 'id'))
+ store.commit('updateBlocks', blocks)
return blocks
})
},
- blockUser (store, id) {
- return store.rootState.api.backendInteractor.blockUser(id)
- .then((user) => store.commit('addNewUsers', [user]))
+ blockUser (store, userId) {
+ return store.rootState.api.backendInteractor.blockUser(userId)
+ .then((relationship) => {
+ store.commit('updateUserRelationship', [relationship])
+ store.commit('removeStatus', { timeline: 'friends', userId })
+ store.commit('removeStatus', { timeline: 'public', userId })
+ store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
+ })
},
unblockUser (store, id) {
return store.rootState.api.backendInteractor.unblockUser(id)
- .then((user) => store.commit('addNewUsers', [user]))
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
- .then((mutedUsers) => {
- each(mutedUsers, (user) => { user.muted = true })
- store.commit('addNewUsers', mutedUsers)
- store.commit('saveMutes', map(mutedUsers, 'id'))
+ .then((mutes) => {
+ store.commit('updateMutes', mutes)
+ store.commit('saveMuteIds', map(mutes, 'id'))
+ return mutes
})
},
muteUser (store, id) {
- return store.state.api.backendInteractor.setUserMute({ id, muted: true })
- .then((user) => store.commit('addNewUsers', [user]))
+ return store.rootState.api.backendInteractor.muteUser(id)
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
},
unmuteUser (store, id) {
- return store.state.api.backendInteractor.setUserMute({ id, muted: false })
- .then((user) => store.commit('addNewUsers', [user]))
+ return store.rootState.api.backendInteractor.unmuteUser(id)
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
},
addFriends ({ rootState, commit }, fetchBy) {
return new Promise((resolve, reject) => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 2de87026..8586f993 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -18,22 +18,16 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
const FOLLOWERS_URL = '/api/statuses/followers.json'
const FRIENDS_URL = '/api/statuses/friends.json'
-const BLOCKS_URL = '/api/statuses/blocks.json'
const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
-const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
const REGISTRATION_URL = '/api/account/register.json'
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
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 +37,15 @@ 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`
+const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
+const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
+const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
+const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
+const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
+const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
import { each, map } from 'lodash'
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -59,6 +62,19 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options)
}
+const promisedRequest = (url, options) => {
+ return fetch(url, options)
+ .then((response) => {
+ return new Promise((resolve, reject) => response.json()
+ .then((json) => {
+ if (!response.ok) {
+ return reject(new StatusCodeError(response.status, json, { url, options }, response))
+ }
+ return resolve(json)
+ }))
+ })
+}
+
// Params
// cropH
// cropW
@@ -211,16 +227,14 @@ const unfollowUser = ({id, credentials}) => {
}
const blockUser = ({id, credentials}) => {
- let url = `${BLOCKING_URL}?user_id=${id}`
- return fetch(url, {
+ return fetch(MASTODON_BLOCK_USER_URL(id), {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const unblockUser = ({id, credentials}) => {
- let url = `${UNBLOCKING_URL}?user_id=${id}`
- return fetch(url, {
+ return fetch(MASTODON_UNBLOCK_USER_URL(id), {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
@@ -243,7 +257,13 @@ const denyUser = ({id, credentials}) => {
}
const fetchUser = ({id, credentials}) => {
- let url = `${USER_URL}?user_id=${id}`
+ let url = `${MASTODON_USER_URL}/${id}`
+ return promisedRequest(url, { headers: authHeaders(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()
@@ -254,7 +274,6 @@ const fetchUser = ({id, credentials}) => {
return resolve(json)
}))
})
- .then((data) => parseUser(data))
}
const fetchFriends = ({id, page, credentials}) => {
@@ -323,23 +342,7 @@ const fetchStatus = ({id, credentials}) => {
.then((data) => parseStatus(data))
}
-const setUserMute = ({id, credentials, muted = true}) => {
- const form = new FormData()
-
- const muteInteger = muted ? 1 : 0
-
- form.append('namespace', 'qvitter')
- form.append('data', muteInteger)
- form.append('topic', `mute:${id}`)
-
- return fetch(QVITTER_USER_PREF_URL, {
- method: 'POST',
- headers: authHeaders(credentials),
- body: form
- })
-}
-
-const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => {
+const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
const timelineUrls = {
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
@@ -347,8 +350,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 +360,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`
}
@@ -374,6 +378,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
}
params.push(['count', 20])
+ params.push(['with_muted', withMuted])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
@@ -522,30 +527,40 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
}
const fetchMutes = ({credentials}) => {
- const url = '/api/qvitter/mutes.json'
-
- return fetch(url, {
- headers: authHeaders(credentials)
- }).then((data) => data.json())
+ return promisedRequest(MASTODON_USER_MUTES_URL, { headers: authHeaders(credentials) })
+ .then((users) => users.map(parseUser))
}
-const fetchBlocks = ({page, credentials}) => {
- return fetch(BLOCKS_URL, {
- headers: authHeaders(credentials)
- }).then((data) => {
- if (data.ok) {
- return data.json()
- }
- throw new Error('Error fetching blocks', data)
+const muteUser = ({id, credentials}) => {
+ return promisedRequest(MASTODON_MUTE_USER_URL(id), {
+ headers: authHeaders(credentials),
+ method: 'POST'
})
}
+const unmuteUser = ({id, credentials}) => {
+ return promisedRequest(MASTODON_UNMUTE_USER_URL(id), {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ })
+}
+
+const fetchBlocks = ({credentials}) => {
+ return promisedRequest(MASTODON_USER_BLOCKS_URL, { headers: authHeaders(credentials) })
+ .then((users) => users.map(parseUser))
+}
+
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
- }).then((data) => data.json())
+ }).then((data) => {
+ if (data.ok) {
+ return data.json()
+ }
+ throw new Error('Error fetching auth tokens', data)
+ })
}
const revokeOAuthToken = ({id, credentials}) => {
@@ -588,6 +603,7 @@ const apiService = {
blockUser,
unblockUser,
fetchUser,
+ fetchUserRelationship,
favorite,
unfavorite,
retweet,
@@ -596,8 +612,9 @@ const apiService = {
deleteStatus,
uploadMedia,
fetchAllFollowing,
- setUserMute,
fetchMutes,
+ muteUser,
+ unmuteUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 7e972d7b..0f0bcddc 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})
}
@@ -58,12 +62,10 @@ const backendInteractorService = (credentials) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
}
- const setUserMute = ({id, muted = true}) => {
- return apiService.setUserMute({id, muted, credentials})
- }
-
const fetchMutes = () => apiService.fetchMutes({credentials})
- const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
+ const muteUser = (id) => apiService.muteUser({credentials, id})
+ const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
+ const fetchBlocks = () => apiService.fetchBlocks({credentials})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
@@ -92,11 +94,13 @@ const backendInteractorService = (credentials) => {
blockUser,
unblockUser,
fetchUser,
+ fetchUserRelationship,
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetching,
- setUserMute,
fetchMutes,
+ muteUser,
+ unmuteUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index d20ce77f..57a6adf9 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -39,11 +39,11 @@ export const parseUser = (data) => {
return output
}
- output.name = null // missing
- output.name_html = data.display_name
+ // output.name = ??? missing
+ output.name_html = addEmojis(data.display_name, data.emojis)
- output.description = null // missing
- output.description_html = data.note
+ // output.description = ??? missing
+ output.description_html = addEmojis(data.note, data.emojis)
// Utilize avatar_static for gif avatars?
output.profile_image_url = data.avatar
@@ -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
@@ -142,6 +146,14 @@ const parseAttachment = (data) => {
return output
}
+export const addEmojis = (string, emojis) => {
+ return emojis.reduce((acc, emoji) => {
+ return acc.replace(
+ new RegExp(`:${emoji.shortcode}:`, 'g'),
+ ``
+ )
+ }, string)
+}
export const parseStatus = (data) => {
const output = {}
@@ -157,7 +169,7 @@ export const parseStatus = (data) => {
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
- output.statusnet_html = data.content
+ output.statusnet_html = addEmojis(data.content, data.emojis)
// Not exactly the same but works?
output.text = data.content
@@ -166,7 +178,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
@@ -176,11 +188,10 @@ export const parseStatus = (data) => {
}
output.summary = data.spoiler_text
- output.summary_html = data.spoiler_text
+ output.summary_html = addEmojis(data.spoiler_text, data.emojis)
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 +270,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)
@@ -282,5 +293,5 @@ export const parseNotification = (data) => {
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
- return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex)
+ return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
diff --git a/src/services/new_api/user_search.js b/src/services/new_api/user_search.js
index ce7da88e..869afa9c 100644
--- a/src/services/new_api/user_search.js
+++ b/src/services/new_api/user_search.js
@@ -1,13 +1,16 @@
import utils from './utils.js'
+import { parseUser } from '../entity_normalizer/entity_normalizer.service.js'
const search = ({query, store}) => {
return utils.request({
store,
- url: '/api/pleroma/search_user',
+ url: '/api/v1/accounts/search',
params: {
- query
+ q: query
}
- }).then((data) => data.json())
+ })
+ .then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const UserSearch = {
search
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 6f99616f..8e954cdf 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -19,6 +19,9 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
const args = { timeline, credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+ const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined'
+ ? rootState.instance.hideMutedPosts
+ : rootState.config.hideMutedPosts
if (older) {
args['until'] = until || timelineData.minId
@@ -28,6 +31,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
args['userId'] = userId
args['tag'] = tag
+ args['withMuted'] = !hideMutedPosts
const numStatusesBeforeFetch = timelineData.statuses.length
diff --git a/src/services/version/version.service.js b/src/services/version/version.service.js
new file mode 100644
index 00000000..a750b0dd
--- /dev/null
+++ b/src/services/version/version.service.js
@@ -0,0 +1,6 @@
+
+export const extractCommit = versionString => {
+ const regex = /-g(\w+)$/i
+ const matches = versionString.match(regex)
+ return matches ? matches[1] : ''
+}
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)
})
})
})
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index 6245361c..2b0b0d6d 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -1,4 +1,4 @@
-import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json'
import qvitterapidata from '../../../../fixtures/statuses.json'
@@ -143,6 +143,23 @@ const makeMockNotificationQvitter = (overrides = {}) => {
}, overrides)
}
+const makeMockEmojiMasto = (overrides = [{}]) => {
+ return [
+ Object.assign({
+ shortcode: 'image',
+ static_url: 'https://example.com/image.png',
+ url: 'https://example.com/image.png',
+ visible_in_picker: false
+ }, overrides[0]),
+ Object.assign({
+ shortcode: 'thinking',
+ static_url: 'https://example.com/think.png',
+ url: 'https://example.com/think.png',
+ visible_in_picker: false
+ }, overrides[1])
+ ]
+}
+
parseNotification
parseUser
parseStatus
@@ -218,6 +235,22 @@ describe('API Entities normalizer', () => {
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
})
+
+ it('adds emojis to post content', () => {
+ const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' })
+
+ const parsedPost = parseStatus(post)
+
+ expect(parsedPost).to.have.property('statusnet_html').that.contains(' {
+ const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
+
+ const parsedPost = parseStatus(post)
+
+ expect(parsedPost).to.have.property('summary_html').that.contains(' {
expect(parseUser(local)).to.have.property('is_local', true)
expect(parseUser(remote)).to.have.property('is_local', false)
})
+
+ it('adds emojis to user name', () => {
+ const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
+
+ const parsedUser = parseUser(user)
+
+ expect(parsedUser).to.have.property('name_html').that.contains(' {
+ const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
+
+ const parsedUser = parseUser(user)
+
+ expect(parsedUser).to.have.property('description_html').that.contains(' {
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
})
})
+
+ describe('MastoAPI emoji adder', () => {
+ const emojis = makeMockEmojiMasto()
+ const imageHtml = ''
+ .replace(/"/g, '\'')
+ const thinkHtml = ''
+ .replace(/"/g, '\'')
+
+ it('correctly replaces shortcodes in supplied string', () => {
+ const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
+ expect(result).to.include(thinkHtml)
+ expect(result).to.include(imageHtml)
+ })
+
+ it('handles consecutive emojis correctly', () => {
+ const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
+ expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
+ })
+
+ it('Doesn\'t replace nonexistent emojis', () => {
+ const result = addEmojis('Admin add the :tenshi: emoji', emojis)
+ expect(result).to.equal('Admin add the :tenshi: emoji')
+ })
+ })
})