diff --git a/src/boot/after_store.js b/src/boot/after_store.js index a5f8c978..f5e84cbc 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -97,6 +97,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showInstanceSpecificPanel') copyInstanceOption('scopeOptionsEnabled') copyInstanceOption('formattingOptionsEnabled') + copyInstanceOption('hideMutedPosts') copyInstanceOption('collapseMessageWithSubject') copyInstanceOption('loginMethod') copyInstanceOption('scopeCopy') diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue index e1bfe20b..a4edff03 100644 --- a/src/components/mute_card/mute_card.vue +++ b/src/components/mute_card/mute_card.vue @@ -1,6 +1,6 @@ - \ No newline at end of file + + + diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index b77c5197..1d5f75ed 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -47,6 +47,11 @@ const settings = { pauseOnUnfocusedLocal: user.pauseOnUnfocused, hoverPreviewLocal: user.hoverPreview, + hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined' + ? instance.hideMutedPosts + : user.hideMutedPosts, + hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts), + collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined' ? instance.collapseMessageWithSubject : user.collapseMessageWithSubject, @@ -177,6 +182,9 @@ const settings = { value = filter(value.split('\n'), (word) => trim(word).length > 0) this.$store.dispatch('setOption', { name: 'muteWords', value }) }, + hideMutedPostsLocal (value) { + this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) + }, collapseMessageWithSubjectLocal (value) { this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 17f1f1a1..33dad549 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -36,6 +36,10 @@

{{$t('nav.timeline')}}

- - 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 @@
+ +
+ + + +
diff --git a/src/i18n/en.json b/src/i18n/en.json index 68503f99..ecf9628f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -153,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", 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 155aa2eb..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, diff --git a/src/modules/users.js b/src/modules/users.js index 1fe12fc8..f83ae298 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -102,10 +102,20 @@ export const mutations = { } }) }, - saveBlocks (state, blockIds) { + 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) { @@ -172,34 +182,39 @@ const users = { 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 176f1c18..8586f993 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -18,10 +18,8 @@ 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' @@ -30,8 +28,6 @@ const PROFILE_UPDATE_URL = '/api/account/update_profile.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.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 FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' @@ -44,6 +40,12 @@ 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' @@ -60,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 @@ -212,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()) @@ -245,16 +258,7 @@ const denyUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => { let url = `${MASTODON_USER_URL}/${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) - })) - }) + return promisedRequest(url, { headers: authHeaders(credentials) }) .then((data) => parseUser(data)) } @@ -338,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, @@ -390,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}` @@ -538,24 +527,29 @@ 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' @@ -618,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 cbd0b733..0f0bcddc 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -62,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}) @@ -100,8 +98,9 @@ const backendInteractorService = (credentials) => { fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, startFetching, - setUserMute, fetchMutes, + muteUser, + unmuteUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, 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