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..22d3c007 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -9,12 +9,13 @@ const getStatusnetConfig = async ({ store }) => { 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 + const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site store.dispatch('setInstanceOption', { name: 'name', value: name }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) store.dispatch('setInstanceOption', { name: 'server', value: server }) + store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' }) // TODO: default values for this stuff, added if to not make it break on // my dev config out of the box. @@ -210,6 +211,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/conversation/conversation.vue b/src/components/conversation/conversation.vue index c39a3ed9..c3bbb597 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -13,7 +13,7 @@ :key="status.id" :inlineExpanded="collapsable" :statusoid="status" - :expandable='!expanded' + :expandable='!isExpanded' :focused="focused(status.id)" :inConversation="isExpanded" :highlight="getHighlight()" 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/login_form/login_form.js b/src/components/login_form/login_form.js index fb6dc651..dc917e47 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -31,15 +31,19 @@ const LoginForm = { username: this.user.username, password: this.user.password } - ).then((result) => { + ).then(async (result) => { if (result.error) { this.authError = result.error this.user.password = '' return } this.$store.commit('setToken', result.access_token) - this.$store.dispatch('loginUser', result.access_token) - this.$router.push({name: 'friends'}) + try { + await this.$store.dispatch('loginUser', result.access_token) + this.$router.push({name: 'friends'}) + } catch (e) { + console.log(e) + } }) }) }, 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/notifications/notifications.js b/src/components/notifications/notifications.js index d3db4b29..d9ce7604 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -10,13 +10,6 @@ const Notifications = { props: [ 'noHeading' ], - created () { - const store = this.$store - const credentials = store.state.users.currentUser.credentials - - const fetcherId = notificationsFetcher.startFetching({ store, credentials }) - this.$store.commit('setNotificationFetcher', { fetcherId }) - }, data () { return { bottomedOut: false 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/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 925a44ec..847cdf1d 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -185,6 +185,9 @@ const PostStatusForm = { }, postFormats () { return this.$store.state.instance.postFormats || [] + }, + safeDMEnabled () { + return this.$store.state.instance.safeDM } }, methods: { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 0579836c..773ccc58 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -3,13 +3,16 @@
{{ $t('post_status.account_not_locked_warning_link') }} -

{{ $t('post_status.direct_warning') }}

+

+ {{ $t('post_status.direct_warning_to_first_only') }} + {{ $t('post_status.direct_warning_to_all') }} +

+ +
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 1df06fe6..d55d1517 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' @@ -90,7 +91,7 @@ const UserProfile = { methods: { startFetchFavorites () { if (this.isUs) { - this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId: this.userId }) } }, fetchUserId () { @@ -118,8 +119,8 @@ const UserProfile = { }, startUp () { if (this.userId) { - this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId }) - this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId: this.userId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId: this.userId }) this.startFetchFavorites() } }, @@ -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 b7a43963..935c1d05 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", @@ -85,7 +86,8 @@ }, "content_warning": "Subject (optional)", "default": "Just landed in L.A.", - "direct_warning": "This post will only be visible to all the mentioned users.", + "direct_warning_to_all": "This post will be visible to all the mentioned users.", + "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "posting": "Posting", "scope": { "direct": "Direct - Post to mentioned users only", @@ -407,7 +409,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/api.js b/src/modules/api.js index 31cb55c6..7ed3edac 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -13,11 +13,11 @@ const api = { setBackendInteractor (state, backendInteractor) { state.backendInteractor = backendInteractor }, - addFetcher (state, {timeline, fetcher}) { - state.fetchers[timeline] = fetcher + addFetcher (state, { fetcherName, fetcher }) { + state.fetchers[fetcherName] = fetcher }, - removeFetcher (state, {timeline}) { - delete state.fetchers[timeline] + removeFetcher (state, { fetcherName }) { + delete state.fetchers[fetcherName] }, setWsToken (state, token) { state.wsToken = token @@ -33,17 +33,24 @@ const api = { } }, actions: { - startFetching (store, {timeline = 'friends', tag = false, userId = false}) { + startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { // Don't start fetching if we already are. if (store.state.fetchers[timeline]) return - const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag }) - store.commit('addFetcher', { timeline, fetcher }) + const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag }) + store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, - stopFetching (store, timeline) { - const fetcher = store.state.fetchers[timeline] + startFetchingNotifications (store) { + // Don't start fetching if we already are. + if (store.state.fetchers['notifications']) return + + const fetcher = store.state.backendInteractor.startFetchingNotifications({ store }) + store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) + }, + stopFetching (store, fetcherName) { + const fetcher = store.state.fetchers[fetcherName] window.clearInterval(fetcher) - store.commit('removeFetcher', {timeline}) + store.commit('removeFetcher', { fetcherName }) }, setWsToken (store, token) { store.commit('setWsToken', token) diff --git a/src/modules/instance.js b/src/modules/instance.js index 3a559ba0..d4185f6a 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -5,6 +5,7 @@ const defaultState = { // Stuff from static/config.json and apiConfig name: 'Pleroma FE', registrationOpen: true, + safeDM: true, textlimit: 5000, server: 'http://localhost:4040/', theme: 'pleroma-dark', diff --git a/src/modules/interface.js b/src/modules/interface.js index 71554787..5b2762e5 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -48,7 +48,6 @@ const interfaceMod = { commit('setNotificationPermission', permission) }, setMobileLayout ({ commit }, value) { - console.log('setMobileLayout called') commit('setMobileLayout', value) } } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 8e0203e3..3c34e533 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -20,20 +20,21 @@ const emptyTl = (userId = 0) => ({ flushMarker: 0 }) +const emptyNotifications = () => ({ + desktopNotificationSilence: true, + maxId: 0, + minId: Number.POSITIVE_INFINITY, + data: [], + idStore: {}, + loading: false, + error: false +}) + export const defaultState = () => ({ allStatuses: [], allStatusesObject: {}, maxId: 0, - notifications: { - desktopNotificationSilence: true, - maxId: 0, - minId: Number.POSITIVE_INFINITY, - data: [], - idStore: {}, - loading: false, - error: false, - fetcherId: null - }, + notifications: emptyNotifications(), favorites: new Set(), error: false, timelines: { @@ -340,9 +341,6 @@ export const mutations = { oldTimeline.visibleStatusesObject = {} each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, - setNotificationFetcher (state, { fetcherId }) { - state.notifications.fetcherId = fetcherId - }, resetStatuses (state) { const emptyState = defaultState() Object.entries(emptyState).forEach(([key, value]) => { @@ -352,6 +350,9 @@ export const mutations = { clearTimeline (state, { timeline }) { state.timelines[timeline] = emptyTl(state.timelines[timeline].userId) }, + clearNotifications (state) { + state.notifications = emptyNotifications() + }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] newStatus.favorited = value @@ -378,6 +379,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 }, @@ -428,16 +436,13 @@ const statuses = { setNotificationsSilence ({ rootState, commit }, { value }) { commit('setNotificationsSilence', { value }) }, - stopFetchingNotifications ({ rootState, commit }) { - if (rootState.statuses.notifications.fetcherId) { - window.clearInterval(rootState.statuses.notifications.fetcherId) - } - commit('setNotificationFetcher', { fetcherId: null }) - }, deleteStatus ({ rootState, commit }, status) { 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..becf66d2 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) @@ -331,7 +353,8 @@ const users = { store.commit('setToken', false) store.dispatch('stopFetching', 'friends') store.commit('setBackendInteractor', backendInteractorService()) - store.dispatch('stopFetchingNotifications') + store.dispatch('stopFetching', 'notifications') + store.commit('clearNotifications') store.commit('resetStatuses') }, loginUser (store, accessToken) { @@ -363,7 +386,10 @@ const users = { } // Start getting fresh posts. - store.dispatch('startFetching', { timeline: 'friends' }) + store.dispatch('startFetchingTimeline', { timeline: 'friends' }) + + // Start fetching notifications + store.dispatch('startFetchingNotifications') // Get user mutes store.dispatch('fetchMutes') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 276ae57c..94725510 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' @@ -354,6 +358,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, @@ -697,6 +781,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 3c780492..20327b58 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,5 +1,6 @@ import apiService from '../api/api.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' +import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' const backendInteractorService = (credentials) => { const fetchStatus = ({id}) => { @@ -58,6 +59,38 @@ const backendInteractorService = (credentials) => { return apiService.denyUser({credentials, id}) } + const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => { + return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) + } + + const startFetchingNotifications = ({ store }) => { + return notificationsFetcher.startFetching({ store, credentials }) + } + + 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 vote = (pollID, optionName) => { return apiService.vote({credentials, pollID, optionName}) } @@ -66,10 +99,6 @@ const backendInteractorService = (credentials) => { return apiService.fetchPoll({credentials, pollID}) } - const startFetching = ({timeline, store, userId = false, tag}) => { - return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag}) - } - const fetchMutes = () => apiService.fetchMutes({credentials}) const muteUser = (id) => apiService.muteUser({credentials, id}) const unmuteUser = (id) => apiService.unmuteUser({credentials, id}) @@ -105,13 +134,20 @@ const backendInteractorService = (credentials) => { fetchUserRelationship, fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, - startFetching, + startFetchingTimeline, + startFetchingNotifications, fetchMutes, muteUser, unmuteUser, 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 c72d0f95..c546f6b1 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"