From 07b8115a37a22b2a7d935154e8231c8a90f199d6 Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 19 Mar 2019 14:36:27 -0400 Subject: #444 - show `remote follow` button when logged out --- src/components/user_card/user_card.vue | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) (limited to 'src/components/user_card/user_card.vue') diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 690e1bde..f4114e6e 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -84,14 +84,8 @@ -
-
- - - -
+
+
@@ -375,11 +369,6 @@ min-height: 28px; } - .remote-follow { - max-width: 220px; - min-height: 28px; - } - .follow { max-width: 220px; min-height: 28px; -- cgit v1.2.3-70-g09d2 From 3255950b0e9a16f2a477d606b91d90bed8a6cef7 Mon Sep 17 00:00:00 2001 From: taehoon Date: Sun, 24 Feb 2019 03:02:04 -0500 Subject: Add mute/unmute featrue and mutes management tab --- src/components/mute_card/mute_card.js | 2 +- src/components/user_card/user_card.js | 20 +++++----- src/components/user_card/user_card.vue | 4 +- src/components/user_settings/user_settings.vue | 6 +++ src/modules/users.js | 32 ++++++++++------ src/services/api/api.service.js | 44 ++++++++++++++++++++-- .../backend_interactor_service.js | 6 ++- src/utils/url.js | 9 +++++ 8 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 src/utils/url.js (limited to 'src/components/user_card/user_card.vue') diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js index 65c9cfb5..5ef17b60 100644 --- a/src/components/mute_card/mute_card.js +++ b/src/components/mute_card/mute_card.js @@ -12,7 +12,7 @@ const MuteCard = { return this.$store.getters.findUser(this.userId) }, muted () { - return this.user.muted + return this.user.mastodonMuted } }, components: { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index b07da675..61b784fe 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -121,21 +121,19 @@ 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) + }, + muteUser () { // Mastodon Mute + this.$store.dispatch('muteUser', this.user.id) + }, + unmuteUser () { // Mastodon Unmute + this.$store.dispatch('unmuteUser', this.user.id) }, toggleMute () { + // TODO: Pleroma mute/unmute, Need to migrate to the Mastodon API const store = this.$store store.commit('setMuted', {user: this.user, muted: !this.user.muted}) store.state.api.backendInteractor.setUserMute(this.user) diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index f4114e6e..3259d1c5 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -74,12 +74,12 @@
- - 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/modules/users.js b/src/modules/users.js index 1fe12fc8..9c89f34a 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -177,9 +177,14 @@ const users = { 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((blockedUser) => { + store.commit('addNewUsers', [blockedUser]) + 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) @@ -188,18 +193,26 @@ const users = { fetchMutes (store) { return store.rootState.api.backendInteractor.fetchMutes() .then((mutedUsers) => { - each(mutedUsers, (user) => { user.muted = true }) + each(mutedUsers, (user) => { user.mastodonMuted = true }) store.commit('addNewUsers', mutedUsers) store.commit('saveMutes', map(mutedUsers, 'id')) }) }, 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(() => { + const user = store.rootState.users.usersObject[id] + set(user, 'mastodonMuted', true) + store.commit('addNewUsers', [user]) + }) }, 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(() => { + const user = store.rootState.users.usersObject[id] + set(user, 'mastodonMuted', false) + store.commit('addNewUsers', [user]) + }) }, addFriends ({ rootState, commit }, fetchBy) { return new Promise((resolve, reject) => { @@ -350,9 +363,6 @@ const users = { // Start getting fresh posts. store.dispatch('startFetching', { timeline: 'friends' }) - // Get user mutes - store.dispatch('fetchMutes') - // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) .then((friends) => commit('addNewUsers', friends)) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 176f1c18..92abf94b 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,3 +1,5 @@ +import { generateUrl } from '../../utils/url' + /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' @@ -19,6 +21,9 @@ 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 MUTES_URL = '/api/v1/mutes.json' +const MUTING_URL = '/api/v1/accounts/:id/mute' +const UNMUTING_URL = '/api/v1/accounts/:id/unmute' 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' @@ -538,14 +543,43 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma } const fetchMutes = ({credentials}) => { - const url = '/api/qvitter/mutes.json' + return fetch(MUTES_URL, { + headers: authHeaders(credentials) + }).then((data) => { + if (data.ok) { + return data.json() + } + throw new Error('Error fetching mutes', data) + }) +} +const muteUser = ({id, credentials}) => { + const url = generateUrl(MUTING_URL, { id }) return fetch(url, { - headers: authHeaders(credentials) - }).then((data) => data.json()) + headers: authHeaders(credentials), + method: 'POST' + }).then((data) => { + if (data.ok) { + return data.json() + } + throw new Error('Error muting', data) + }) +} + +const unmuteUser = ({id, credentials}) => { + const url = generateUrl(UNMUTING_URL, { id }) + return fetch(url, { + headers: authHeaders(credentials), + method: 'POST' + }).then((data) => { + if (data.ok) { + return data.json() + } + throw new Error('Error unmuting', data) + }) } -const fetchBlocks = ({page, credentials}) => { +const fetchBlocks = ({credentials}) => { return fetch(BLOCKS_URL, { headers: authHeaders(credentials) }).then((data) => { @@ -620,6 +654,8 @@ const apiService = { 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..674fb4a4 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -67,7 +67,9 @@ const backendInteractorService = (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}) @@ -102,6 +104,8 @@ const backendInteractorService = (credentials) => { startFetching, setUserMute, fetchMutes, + muteUser, + unmuteUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, diff --git a/src/utils/url.js b/src/utils/url.js new file mode 100644 index 00000000..79ea7394 --- /dev/null +++ b/src/utils/url.js @@ -0,0 +1,9 @@ +// Generate url based on template +// Example: /api/v1/accounts/:id/mute -> /api/v1/accounts/123/mute +export const generateUrl = (template, params = {}) => { + let url = template + Object.entries(params).forEach(([key, value]) => { + url = url.replace(':' + key, value) + }) + return url +} -- cgit v1.2.3-70-g09d2 From 6b6878bde06b375b1e715a3557f153acc73a8af0 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Mon, 18 Feb 2019 17:49:32 +0300 Subject: Added moderation menu --- package.json | 2 + src/App.scss | 8 ++ src/boot/after_store.js | 1 + src/components/delete_button/delete_button.js | 6 +- src/components/dialog_modal/dialog_modal.js | 14 ++ src/components/dialog_modal/dialog_modal.vue | 92 ++++++++++++ .../moderation_tools/moderation_tools.js | 106 ++++++++++++++ .../moderation_tools/moderation_tools.vue | 158 +++++++++++++++++++++ src/components/notification/notification.js | 3 + src/components/notification/notification.vue | 2 +- src/components/popper/popper.scss | 70 +++++++++ src/components/user_card/user_card.js | 13 +- src/components/user_card/user_card.vue | 2 + src/components/user_profile/user_profile.js | 4 +- src/i18n/en.json | 24 +++- src/i18n/ru.json | 24 +++- src/modules/statuses.js | 10 ++ src/modules/users.js | 22 +++ src/services/api/api.service.js | 90 ++++++++++++ .../backend_interactor_service.js | 30 ++++ .../entity_normalizer/entity_normalizer.service.js | 19 ++- yarn.lock | 10 ++ 22 files changed, 697 insertions(+), 13 deletions(-) create mode 100644 src/components/dialog_modal/dialog_modal.js create mode 100644 src/components/dialog_modal/dialog_modal.vue create mode 100644 src/components/moderation_tools/moderation_tools.js create mode 100644 src/components/moderation_tools/moderation_tools.vue create mode 100644 src/components/popper/popper.scss (limited to 'src/components/user_card/user_card.vue') diff --git a/package.json b/package.json index 03228133..c80e0f63 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,14 @@ "node-sass": "^3.10.1", "object-path": "^0.11.3", "phoenix": "^1.3.0", + "popper.js": "^1.14.7", "sanitize-html": "^1.13.0", "sass-loader": "^4.0.2", "vue": "^2.5.13", "vue-chat-scroll": "^1.2.1", "vue-compose": "^0.7.1", "vue-i18n": "^7.3.2", + "vue-popperjs": "^2.0.3", "vue-router": "^3.0.1", "vue-template-compiler": "^2.3.4", "vue-timeago": "^3.1.2", diff --git a/src/App.scss b/src/App.scss index 5fc0dd27..b1c65ade 100644 --- a/src/App.scss +++ b/src/App.scss @@ -101,6 +101,14 @@ button { background-color: $fallback--bg; background-color: var(--bg, $fallback--bg) } + + &.danger { + // TODO: add better color variable + color: $fallback--text; + color: var(--alertErrorPanelText, $fallback--text); + background-color: $fallback--alertError; + background-color: var(--alertError, $fallback--alertError); + } } label.select { diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 862a534d..bb593026 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -210,6 +210,7 @@ const getNodeInfo = async ({ store }) => { const frontendVersion = window.___pleromafe_commit_hash store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) + store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') }) } else { throw (res) } diff --git a/src/components/delete_button/delete_button.js b/src/components/delete_button/delete_button.js index f2920666..22f24625 100644 --- a/src/components/delete_button/delete_button.js +++ b/src/components/delete_button/delete_button.js @@ -10,7 +10,11 @@ const DeleteButton = { }, computed: { currentUser () { return this.$store.state.users.currentUser }, - canDelete () { return this.currentUser && this.currentUser.rights.delete_others_notice || this.status.user.id === this.currentUser.id } + canDelete () { + if (!this.currentUser) { return } + const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin + return superuser || this.status.user.id === this.currentUser.id + } } } diff --git a/src/components/dialog_modal/dialog_modal.js b/src/components/dialog_modal/dialog_modal.js new file mode 100644 index 00000000..f14e3fe9 --- /dev/null +++ b/src/components/dialog_modal/dialog_modal.js @@ -0,0 +1,14 @@ +const DialogModal = { + props: { + darkOverlay: { + default: true, + type: Boolean + }, + onCancel: { + default: () => {}, + type: Function + } + } +} + +export default DialogModal diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue new file mode 100644 index 00000000..7621fb20 --- /dev/null +++ b/src/components/dialog_modal/dialog_modal.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js new file mode 100644 index 00000000..3eedeaa1 --- /dev/null +++ b/src/components/moderation_tools/moderation_tools.js @@ -0,0 +1,106 @@ +import DialogModal from '../dialog_modal/dialog_modal.vue' +import Popper from 'vue-popperjs/src/component/popper.js.vue' + +const FORCE_NSFW = 'mrf_tag:media-force-nsfw' +const STRIP_MEDIA = 'mrf_tag:media-strip' +const FORCE_UNLISTED = 'mrf_tag:force-unlisted' +const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription' +const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription' +const SANDBOX = 'mrf_tag:sandbox' +const QUARANTINE = 'mrf_tag:quarantine' + +const ModerationTools = { + props: [ + 'user' + ], + data () { + return { + showDropDown: false, + tags: { + FORCE_NSFW, + STRIP_MEDIA, + FORCE_UNLISTED, + DISABLE_REMOTE_SUBSCRIPTION, + DISABLE_ANY_SUBSCRIPTION, + SANDBOX, + QUARANTINE + }, + showDeleteUserDialog: false + } + }, + components: { + DialogModal, + Popper + }, + computed: { + tagsSet () { + return new Set(this.user.tags) + }, + hasTagPolicy () { + return this.$store.state.instance.tagPolicyAvailable + } + }, + methods: { + toggleMenu () { + this.showDropDown = !this.showDropDown + }, + hasTag (tagName) { + return this.tagsSet.has(tagName) + }, + toggleTag (tag) { + const store = this.$store + if (this.tagsSet.has(tag)) { + store.state.api.backendInteractor.untagUser(this.user, tag).then(response => { + if (!response.ok) { return } + store.commit('untagUser', {user: this.user, tag}) + }) + } else { + store.state.api.backendInteractor.tagUser(this.user, tag).then(response => { + if (!response.ok) { return } + store.commit('tagUser', {user: this.user, tag}) + }) + } + }, + toggleRight (right) { + const store = this.$store + if (this.user.rights[right]) { + store.state.api.backendInteractor.deleteRight(this.user, right).then(response => { + if (!response.ok) { return } + store.commit('updateRight', {user: this.user, right: right, value: false}) + }) + } else { + store.state.api.backendInteractor.addRight(this.user, right).then(response => { + if (!response.ok) { return } + store.commit('updateRight', {user: this.user, right: right, value: true}) + }) + } + }, + toggleActivationStatus () { + const store = this.$store + const status = !!this.user.deactivated + store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => { + if (!response.ok) { return } + store.commit('updateActivationStatus', {user: this.user, status: status}) + }) + }, + deleteUserDialog (show) { + this.showDeleteUserDialog = show + }, + deleteUser () { + const store = this.$store + const user = this.user + const {id, name} = user + store.state.api.backendInteractor.deleteUser(user) + .then(e => { + this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id) + const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile' + const isTargetUser = this.$route.params.name === name || this.$route.params.id === id + if (isProfile && isTargetUser) { + window.history.back() + } + }) + } + } +} + +export default ModerationTools diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue new file mode 100644 index 00000000..c24a2280 --- /dev/null +++ b/src/components/moderation_tools/moderation_tools.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 42a48f3f..6aa6101f 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -21,6 +21,9 @@ const Notification = { }, userProfileLink (user) { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) + }, + getUser (notification) { + return this.$store.state.users.usersObject[notification.action.user.id] } }, computed: { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 8f532747..cac335ce 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -5,7 +5,7 @@
- +
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss new file mode 100644 index 00000000..0c30d625 --- /dev/null +++ b/src/components/popper/popper.scss @@ -0,0 +1,70 @@ +@import '../../_variables.scss'; + +.popper-wrapper { + z-index: 8; +} + +.popper-wrapper .popper__arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 5px; +} + +.popper-wrapper[x-placement^="top"] { + margin-bottom: 5px; +} + +.popper-wrapper[x-placement^="top"] .popper__arrow { + border-width: 5px 5px 0 5px; + border-color: $fallback--bg transparent transparent transparent; + border-color: var(--bg, $fallback--bg) transparent transparent transparent; + bottom: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} + +.popper-wrapper[x-placement^="bottom"] { + margin-top: 5px; +} + +.popper-wrapper[x-placement^="bottom"] .popper__arrow { + border-width: 0 5px 5px 5px; + border-color: transparent transparent $fallback--bg transparent; + border-color: transparent transparent var(--bg, $fallback--bg) transparent; + top: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} + +.popper-wrapper[x-placement^="right"] { + margin-left: 5px; +} + +.popper-wrapper[x-placement^="right"] .popper__arrow { + border-width: 5px 5px 5px 0; + border-color: transparent $fallback--bg transparent transparent; + border-color: transparent var(--bg, $fallback--bg) transparent transparent; + left: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + +.popper-wrapper[x-placement^="left"] { + margin-right: 5px; +} + +.popper-wrapper[x-placement^="left"] .popper__arrow { + border-width: 5px 0 5px 5px; + border-color: transparent transparent transparent $fallback--bg; + border-color: transparent transparent transparent var(--bg, $fallback--bg); + right: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 197c61d5..1a100de3 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,5 +1,6 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' +import ModerationTools from '../moderation_tools/moderation_tools.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' @@ -93,15 +94,17 @@ export default { } }, visibleRole () { - const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') - const showRole = this.isOtherUser || this.user.show_role - - return validRole && showRole && this.user.role + const rights = this.user.rights + if (!rights) { return } + const validRole = rights.admin || rights.moderator + const roleTitle = rights.admin ? 'admin' : 'moderator' + return validRole && roleTitle } }, components: { UserAvatar, - RemoteFollow + RemoteFollow, + ModerationTools }, methods: { followUser () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 3259d1c5..b5c7fa24 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -99,6 +99,8 @@
+ +
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 1df06fe6..540de955 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -3,6 +3,7 @@ import get from 'lodash/get' import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' +import ModerationTools from '../moderation_tools/moderation_tools.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' import withList from '../../hocs/with_list/with_list' @@ -155,7 +156,8 @@ const UserProfile = { UserCard, Timeline, FollowerList, - FriendList + FriendList, + ModerationTools } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 026546cc..0bb58734 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -22,7 +22,8 @@ "generic_error": "An error occured", "optional": "optional", "show_more": "Show more", - "show_less": "Show less" + "show_less": "Show less", + "cancel": "Cancel" }, "image_cropper": { "crop_picture": "Crop picture", @@ -402,7 +403,26 @@ "block_progress": "Blocking...", "unmute": "Unmute", "unmute_progress": "Unmuting...", - "mute_progress": "Muting..." + "mute_progress": "Muting...", + "admin_menu": { + "moderation": "Moderation", + "grant_admin": "Grant Admin", + "revoke_admin": "Revoke Admin", + "grant_moderator": "Grant Moderator", + "revoke_moderator": "Revoke Moderator", + "activate_account": "Activate account", + "deactivate_account": "Deactivate account", + "delete_account": "Delete account", + "force_nsfw": "Mark all posts as NSFW", + "strip_media": "Remove media from posts", + "force_unlisted": "Force posts to be unlisted", + "sandbox": "Force posts to be followers-only", + "disable_remote_subscription": "Disallow following user from remote instances", + "disable_any_subscription": "Disallow following user at all", + "quarantine": "Disallow user posts from federating", + "delete_user": "Delete user", + "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone." + } }, "user_profile": { "timeline_title": "User Timeline", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 89aa43f4..5450f154 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -8,7 +8,8 @@ }, "general": { "apply": "Применить", - "submit": "Отправить" + "submit": "Отправить", + "cancel": "Отмена" }, "login": { "login": "Войти", @@ -311,7 +312,26 @@ "muted": "Игнорирую", "per_day": "в день", "remote_follow": "Читать удалённо", - "statuses": "Статусы" + "statuses": "Статусы", + "admin_menu": { + "moderation": "Опции модератора", + "grant_admin": "Сделать администратором", + "revoke_admin": "Забрать права администратора", + "grant_moderator": "Сделать модератором", + "revoke_moderator": "Забрать права модератора", + "activate_account": "Активировать аккаунт", + "deactivate_account": "Деактивировать аккаунт", + "delete_account": "Удалить аккаунт", + "force_nsfw": "Отмечать посты пользователя как NSFW", + "strip_media": "Убирать вложения из постов пользователя", + "force_unlisted": "Не добавлять посты в публичные ленты", + "sandbox": "Посты доступны только для подписчиков", + "disable_remote_subscription": "Запретить подписываться с удаленных серверов", + "disable_any_subscription": "Запретить подписываться на пользователя", + "quarantine": "Не федерировать посты пользователя", + "delete_user": "Удалить пользователя", + "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить." + } }, "user_profile": { "timeline_title": "Лента пользователя" diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 8e0203e3..4007b0fc 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -378,6 +378,13 @@ export const mutations = { const newStatus = state.allStatusesObject[status.id] newStatus.deleted = true }, + setManyDeleted (state, condition) { + Object.values(state.allStatusesObject).forEach(status => { + if (condition(status)) { + status.deleted = true + } + }) + }, setLoading (state, { timeline, value }) { state.timelines[timeline].loading = value }, @@ -438,6 +445,9 @@ const statuses = { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) }, + markStatusesAsDeleted ({ commit }, condition) { + commit('setManyDeleted', condition) + }, favorite ({ rootState, commit }, status) { // Optimistic favoriting... commit('setFavorited', { status, value: true }) diff --git a/src/modules/users.js b/src/modules/users.js index 1a507d31..5839574b 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -37,6 +37,28 @@ export const mutations = { const user = state.usersObject[id] set(user, 'muted', muted) }, + tagUser (state, { user: { id }, tag }) { + const user = state.usersObject[id] + const tags = user.tags || [] + const newTags = tags.concat([tag]) + set(user, 'tags', newTags) + }, + untagUser (state, { user: { id }, tag }) { + const user = state.usersObject[id] + const tags = user.tags || [] + const newTags = tags.filter(t => t !== tag) + set(user, 'tags', newTags) + }, + updateRight (state, { user: { id }, right, value }) { + const user = state.usersObject[id] + let newRights = user.rights + newRights[right] = value + set(user, 'rights', newRights) + }, + updateActivationStatus (state, { user: { id }, status }) { + const user = state.usersObject[id] + set(user, 'deactivated', !status) + }, setCurrentUser (state, user) { state.lastLoginName = user.screen_name state.currentUser = merge(state.currentUser || {}, user) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 030c2f5e..bd331062 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -16,6 +16,10 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests' const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const DENY_USER_URL = '/api/pleroma/friendships/deny' +const TAG_USER_URL = '/api/pleroma/admin/users/tag' +const PERMISSION_GROUP_URL = '/api/pleroma/admin/permission_group' +const ACTIVATION_STATUS_URL = '/api/pleroma/admin/activation_status' +const ADMIN_USER_URL = '/api/pleroma/admin/user' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' @@ -352,6 +356,86 @@ const fetchStatus = ({id, credentials}) => { .then((data) => parseStatus(data)) } +const tagUser = ({tag, credentials, ...options}) => { + const screenName = options.screen_name + const form = { + nicknames: [screenName], + tags: [tag] + } + + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(TAG_USER_URL, { + method: 'PUT', + headers: headers, + body: JSON.stringify(form) + }) +} + +const untagUser = ({tag, credentials, ...options}) => { + const screenName = options.screen_name + const body = { + nicknames: [screenName], + tags: [tag] + } + + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(TAG_USER_URL, { + method: 'DELETE', + headers: headers, + body: JSON.stringify(body) + }) +} + +const addRight = ({right, credentials, ...user}) => { + const screenName = user.screen_name + + return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, { + method: 'POST', + headers: authHeaders(credentials), + body: {} + }) +} + +const deleteRight = ({right, credentials, ...user}) => { + const screenName = user.screen_name + + return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, { + method: 'DELETE', + headers: authHeaders(credentials), + body: {} + }) +} + +const setActivationStatus = ({status, credentials, ...user}) => { + const screenName = user.screen_name + const body = { + status: status + } + + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(`${ACTIVATION_STATUS_URL}/${screenName}.json`, { + method: 'PUT', + headers: headers, + body: JSON.stringify(body) + }) +} + +const deleteUser = ({credentials, ...user}) => { + const screenName = user.screen_name + const headers = authHeaders(credentials) + + return fetch(`${ADMIN_USER_URL}.json?nickname=${screenName}`, { + method: 'DELETE', + headers: headers + }) +} + const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, @@ -666,6 +750,12 @@ const apiService = { fetchBlocks, fetchOAuthTokens, revokeOAuthToken, + tagUser, + untagUser, + deleteUser, + addRight, + deleteRight, + setActivationStatus, register, getCaptcha, updateAvatar, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 71e78d2f..badcbb06 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -62,6 +62,30 @@ const backendInteractorService = (credentials) => { return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag}) } + const tagUser = ({screen_name}, tag) => { + return apiService.tagUser({screen_name, tag, credentials}) + } + + const untagUser = ({screen_name}, tag) => { + return apiService.untagUser({screen_name, tag, credentials}) + } + + const addRight = ({screen_name}, right) => { + return apiService.addRight({screen_name, right, credentials}) + } + + const deleteRight = ({screen_name}, right) => { + return apiService.deleteRight({screen_name, right, credentials}) + } + + const setActivationStatus = ({screen_name}, status) => { + return apiService.setActivationStatus({screen_name, status, credentials}) + } + + const deleteUser = ({screen_name}) => { + return apiService.deleteUser({screen_name, credentials}) + } + const fetchMutes = () => apiService.fetchMutes({credentials}) const muteUser = (id) => apiService.muteUser({credentials, id}) const unmuteUser = (id) => apiService.unmuteUser({credentials, id}) @@ -104,6 +128,12 @@ const backendInteractorService = (credentials) => { fetchBlocks, fetchOAuthTokens, revokeOAuthToken, + tagUser, + untagUser, + addRight, + deleteRight, + deleteUser, + setActivationStatus, register, getCaptcha, updateAvatar, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index ea57e6b2..8aa4b352 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -67,6 +67,11 @@ export const parseUser = (data) => { output.statusnet_blocking = relationship.blocking output.muted = relationship.muting } + + output.rights = { + moderator: data.pleroma.is_moderator, + admin: data.pleroma.is_admin + } } // Missing, trying to recover @@ -103,7 +108,12 @@ export const parseUser = (data) => { // QVITTER ONLY FOR NOW // Really only applies to logged in user, really.. I THINK - output.rights = data.rights + if (data.rights) { + output.rights = { + moderator: data.rights.delete_others_notice, + admin: data.rights.admin + } + } output.no_rich_text = data.no_rich_text output.default_scope = data.default_scope output.hide_follows = data.hide_follows @@ -125,6 +135,13 @@ export const parseUser = (data) => { output.follow_request_count = data.pleroma.follow_request_count } + if (data.pleroma) { + output.tags = data.pleroma.tags + output.deactivated = data.pleroma.deactivated + } + + output.tags = output.tags || [] + return output } diff --git a/yarn.lock b/yarn.lock index 0fe45479..58007622 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4831,6 +4831,10 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +popper.js@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -6457,6 +6461,12 @@ vue-loader@^11.1.0: vue-style-loader "^2.0.0" vue-template-es2015-compiler "^1.2.2" +vue-popperjs@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd" + dependencies: + popper.js "^1.14.7" + vue-router@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" -- cgit v1.2.3-70-g09d2 From 6aee2ed63fa479066b2243a03af05cdba68a9ba2 Mon Sep 17 00:00:00 2001 From: Brenden Bice Date: Thu, 11 Apr 2019 23:14:45 -0400 Subject: replace pencil with wrench icon # Conflicts: # src/components/user_card/user_card.vue --- src/components/user_card/user_card.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/components/user_card/user_card.vue') diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index b5c7fa24..a8055621 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -11,7 +11,7 @@
{{user.name}}
- + -- cgit v1.2.3-70-g09d2 From 14d506154e844cbae82489dd4edfd1d31c0559db Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 20 Apr 2019 21:06:34 +0200 Subject: user_card.vue: Fix .emoji to apply to img --- src/components/user_card/user_card.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/components/user_card/user_card.vue') diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index b5c7fa24..bb95f595 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -162,7 +162,7 @@ max-width: 100%; max-height: 400px; - .emoji { + &.emoji { width: 32px; height: 32px; } -- cgit v1.2.3-70-g09d2 From c44c0483566efc8c48361e4143b25d852cecede4 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 1 May 2019 17:33:56 +0300 Subject: add title and alt for avatars, fix console errors in avatarlist --- src/components/avatar_list/avatar_list.js | 6 +++--- src/components/avatar_list/avatar_list.vue | 4 ++-- src/components/basic_user_card/basic_user_card.vue | 6 +++++- src/components/notification/notification.vue | 2 +- src/components/status/status.js | 6 +++--- src/components/status/status.vue | 17 +++++++++++------ src/components/user_avatar/user_avatar.js | 2 +- src/components/user_avatar/user_avatar.vue | 4 +++- src/components/user_card/user_card.vue | 2 +- .../entity_normalizer/entity_normalizer.service.js | 3 +++ 10 files changed, 33 insertions(+), 19 deletions(-) (limited to 'src/components/user_card/user_card.vue') diff --git a/src/components/avatar_list/avatar_list.js b/src/components/avatar_list/avatar_list.js index d65125c2..b9e11aaa 100644 --- a/src/components/avatar_list/avatar_list.js +++ b/src/components/avatar_list/avatar_list.js @@ -1,10 +1,10 @@ import UserAvatar from '../user_avatar/user_avatar.vue' const AvatarList = { - props: ['avatars'], + props: ['users'], computed: { - slicedAvatars () { - return this.avatars ? this.avatars.slice(0, 15) : [] + slicedUsers () { + return this.users ? this.users.slice(0, 15) : [] } }, components: { diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue index b14474ba..4e0de2c9 100644 --- a/src/components/avatar_list/avatar_list.vue +++ b/src/components/avatar_list/avatar_list.vue @@ -1,7 +1,7 @@ diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 25f1fb2a..634d62b3 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -1,7 +1,11 @@ diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js new file mode 100644 index 00000000..324a7597 --- /dev/null +++ b/src/components/checkbox/checkbox.js @@ -0,0 +1,44 @@ +// TODO: Template-based functional component is supported in vue-loader 13.3.0+. +// Also, somehow, props are not provided through 'context' even though they are defined. +// Need to upgrade vue-loader + +import './checkbox.scss' + +export default { + functional: true, + name: 'Checkbox', + model: { + prop: 'checked', + event: 'change' + }, + render (createElement, { data, children }) { + const { props = {}, attrs = {}, on = {}, ...rest } = data + const { name, checked, disabled, readonly, ...restAttrs } = attrs + const { change, ...restListeners } = on + const wrapperProps = { + attrs: restAttrs, + on: restListeners, + ...rest + } + const inputProps = { + attrs: { + name, + checked, + disabled, + readonly, + ...props + }, + on: {} + } + if (change) { + inputProps.on.change = e => change(e.target.checked) + } + return ( + + ) + } +} diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss new file mode 100644 index 00000000..07b3f39e --- /dev/null +++ b/src/components/checkbox/checkbox.scss @@ -0,0 +1,48 @@ +@import '../../_variables.scss'; + +.checkbox { + position: relative; + display: inline-block; + padding-left: 1.2em; + + input[type=checkbox] { + display: none; + + & + i::before { + position: absolute; + left: 0; + top: 0; + display: block; + content: '✔'; + transition: color 200ms; + width: 1.1em; + height: 1.1em; + border-radius: $fallback--checkboxRadius; + border-radius: var(--checkboxRadius, $fallback--checkboxRadius); + box-shadow: 0px 0px 2px black inset; + box-shadow: var(--inputShadow); + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); + vertical-align: top; + text-align: center; + line-height: 1.1em; + font-size: 1.1em; + color: transparent; + overflow: hidden; + box-sizing: border-box; + } + + &:checked + i::before { + color: $fallback--text; + color: var(--text, $fallback--text); + } + + &:disabled + i::before { + opacity: .5; + } + } + + & > span { + margin-left: .5em; + } +} \ No newline at end of file diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 1a100de3..7c6ffa89 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -151,6 +151,9 @@ export default { }, userProfileLink (user) { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) + }, + reportUser () { + this.$store.dispatch('openUserReportingModal', this.user.id) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index e62b384d..2d02ca03 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -99,8 +99,14 @@ - - +
+ + + +
+ diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js new file mode 100644 index 00000000..fb9ea16d --- /dev/null +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -0,0 +1,81 @@ + +import Status from '../status/status.vue' +import Checkbox from '../checkbox/checkbox.js' + +const UserReportingModal = { + components: { + Status, + Checkbox + }, + data () { + return { + comment: '', + forward: false, + statusIdsToReport: [] + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + isOpen () { + return this.isLoggedIn && this.$store.state.reports.modalActivated + }, + userId () { + return this.$store.state.reports.userId + }, + user () { + return this.$store.getters.findUser(this.userId) + }, + remoteInstance () { + return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1) + }, + statuses () { + return this.$store.state.reports.statuses + } + }, + watch: { + userId (value) { + this.statusIdsToReport = [] + } + }, + methods: { + closeModal () { + this.$store.dispatch('closeUserReportingModal') + }, + reportUser () { + const payload = { + comment: this.comment, + forward: this.forward, + statusIdsToReport: this.statusIdsToReport + } + this.$store.dispatch('reportUser', payload) + }, + isChecked (statusId) { + return this.statusIdsToReport.indexOf(statusId) !== -1 + }, + toggleStatus (checked, statusId) { + if (checked === this.isChecked(statusId)) { + return + } + + if (checked) { + this.statusIdsToReport.push(statusId) + } else { + this.statusIdsToReport.splice(this.statusIdsToReport.indexOf(statusId), 1) + } + }, + resize (e) { + const target = e.target || e + if (!(target instanceof window.Element)) { return } + // Auto is needed to make textbox shrink when removing lines + target.style.height = 'auto' + target.style.height = `${target.scrollHeight}px` + if (target.value === '') { + target.style.height = null + } + } + } +} + +export default UserReportingModal diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue new file mode 100644 index 00000000..49839da3 --- /dev/null +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -0,0 +1,111 @@ + \ No newline at end of file + diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 744925d4..faefe1ff 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -1,134 +1,206 @@ diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 6428b1b0..80b75ce5 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -1,63 +1,98 @@ diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 440e1957..e482cfd8 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,187 +1,424 @@ diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index b4495673..7ad8aa1d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -1,134 +1,256 @@ diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue index a118ffe2..39d49237 100644 --- a/src/components/user_finder/user_finder.vue +++ b/src/components/user_finder/user_finder.vue @@ -1,14 +1,38 @@