{{ $t('moderation.reports.reports') }}
+{{ $t('moderation.reports.no_reports') }}
+diff --git a/src/App.js b/src/App.js
index 3690b944..d4b3b41a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue'
+import ModModal from './components/mod_modal/mod_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
@@ -33,6 +34,7 @@ export default {
MobileNav,
DesktopNav,
SettingsModal,
+ ModModal,
UserReportingModal,
PostStatusModal,
EditStatusModal,
diff --git a/src/App.vue b/src/App.vue
index ca114c89..80ebb525 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -61,6 +61,7 @@
{{ $t('moderation.reports.no_reports') }} ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
+
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
@@ -21,6 +24,10 @@ export default data => {
if (firstChar === '@' && usersCurry) {
return usersCurry(input)
}
+ if (firstChar === '$') {
+ return MFM_TAGS
+ .filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
+ }
return []
}
}
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index b5cc1c63..ac7b8b5d 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -1,5 +1,25 @@
@import '../../_variables.scss';
+.Notification {
+ .emoji-picker {
+ min-width: 160%;
+ width: 150%;
+ overflow: hidden;
+ left: -70%;
+ max-width: 100%;
+ @media (min-width: 800px) and (max-width: 1300px) {
+ left: -50%;
+ min-width: 50%;
+ max-width: 130%;
+ }
+
+ @media (max-width: 800px) {
+ left: -10%;
+ min-width: 50%;
+ max-width: 130%;
+ }
+ }
+}
.emoji-picker {
display: flex;
flex-direction: column;
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 6d9713ff..d9c568f6 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -92,7 +92,7 @@
}
}
-.picked-reaction {
+.button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
diff --git a/src/components/mod_modal/mod_modal.js b/src/components/mod_modal/mod_modal.js
new file mode 100644
index 00000000..fb11ef87
--- /dev/null
+++ b/src/components/mod_modal/mod_modal.js
@@ -0,0 +1,58 @@
+import Modal from 'src/components/modal/modal.vue'
+import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
+import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
+import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faTimes,
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+import {
+ faWindowMinimize
+} from '@fortawesome/free-regular-svg-icons'
+
+library.add(
+ faTimes,
+ faWindowMinimize,
+ faChevronDown
+)
+
+const ModModal = {
+ components: {
+ Modal,
+ ModModalContent: getResettableAsyncComponent(
+ () => import('./mod_modal_content.vue'),
+ {
+ loadingComponent: PanelLoading,
+ errorComponent: AsyncComponentError,
+ delay: 0
+ }
+ )
+ },
+ methods: {
+ closeModal () {
+ this.$store.dispatch('closeModModal')
+ },
+ peekModal () {
+ this.$store.dispatch('togglePeekModModal')
+ }
+ },
+ computed: {
+ moderator () {
+ return this.$store.state.users.currentUser &&
+ (this.$store.state.users.currentUser.role === 'admin' ||
+ this.$store.state.users.currentUser.role === 'moderator')
+ },
+ modalActivated () {
+ return this.$store.state.interface.modModalState !== 'hidden'
+ },
+ modalOpenedOnce () {
+ return this.$store.state.interface.modModalLoaded
+ },
+ modalPeeked () {
+ return this.$store.state.interface.modModalState === 'minimized'
+ }
+ }
+}
+
+export default ModModal
diff --git a/src/components/mod_modal/mod_modal.scss b/src/components/mod_modal/mod_modal.scss
new file mode 100644
index 00000000..4821df74
--- /dev/null
+++ b/src/components/mod_modal/mod_modal.scss
@@ -0,0 +1,44 @@
+@import 'src/_variables.scss';
+.mod-modal {
+ overflow: hidden;
+
+ &.peek {
+ .mod-modal-panel {
+ /* Explanation:
+ * Modal is positioned vertically centered.
+ * 100vh - 100% = Distance between modal's top+bottom boundaries and screen
+ * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
+ * + 100% - we move modal completely off-screen, it's top boundary touches
+ * bottom of the screen
+ * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
+ */
+ transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
+
+ @media all and (max-width: 800px) {
+ /* For mobile, the modal takes 100% of the available screen.
+ This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
+ */
+ transform: translateY(calc(100% - 50px));
+ }
+ }
+ }
+
+ .mod-modal-panel {
+ overflow: hidden;
+ transition: transform;
+ transition-timing-function: ease-in-out;
+ transition-duration: 300ms;
+ width: 1000px;
+ max-width: 90vw;
+ height: 90vh;
+
+ @media all and (max-width: 800px) {
+ max-width: 100vw;
+ height: 100%;
+ }
+
+ .panel-body {
+ height: inherit;
+ }
+ }
+}
diff --git a/src/components/mod_modal/mod_modal.vue b/src/components/mod_modal/mod_modal.vue
new file mode 100644
index 00000000..64bbf021
--- /dev/null
+++ b/src/components/mod_modal/mod_modal.vue
@@ -0,0 +1,43 @@
+
+
', '\n')
+ const textarea = document.createElement('textarea')
+ textarea.innerHTML = content
+ return textarea.value
+ },
+ updateReportState (state) {
+ this.$store.dispatch('updateReportStates', { reports: [{ id: this.id, state }] })
+ },
+ toggleNotes () {
+ this.notesHidden = !this.notesHidden
+ },
+ addNoteToReport () {
+ if (this.note.length > 0) {
+ this.$store.dispatch('addNoteToReport', { id: this.id, note: this.note })
+ this.note = null
+ }
+ },
+ toggleStatuses () {
+ this.statusesHidden = !this.statusesHidden
+ },
+ hasTag (tag) {
+ return this.user.tags.includes(tag)
+ },
+ toggleTag (tag) {
+ if (this.hasTag(tag)) {
+ this.$store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
+ if (!response.ok) { return }
+ this.$store.commit('untagUser', { user: this.user, tag })
+ })
+ } else {
+ this.$store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
+ if (!response.ok) { return }
+ this.$store.commit('tagUser', { user: this.user, tag })
+ })
+ }
+ },
+ toggleActivationStatus () {
+ this.$store.dispatch('toggleActivationStatus', { user: this.user })
+ },
+ deleteUser () {
+ this.$store.state.backendInteractor.deleteUser({ user: this.user })
+ .then(e => {
+ this.$store.dispatch('markStatusesAsDeleted', status => this.user.id === status.user.id)
+ const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
+ const isTargetUser = this.$route.params.name === this.user.name || this.$route.params.id === this.user.id
+ if (isProfile && isTargetUser) {
+ window.history.back()
+ }
+ })
+ }
+ }
+}
+
+export default ReportCard
diff --git a/src/components/mod_modal/tabs/reports_tab/report_card.vue b/src/components/mod_modal/tabs/reports_tab/report_card.vue
new file mode 100644
index 00000000..6cc034b1
--- /dev/null
+++ b/src/components/mod_modal/tabs/reports_tab/report_card.vue
@@ -0,0 +1,202 @@
+
+ {{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}
+
+
+
+ {{ $t('moderation.reports.reports') }}
+
', '\n')
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll(''', "'")
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll(''', "'")
)
})
}
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
index 551649c7..73dd3ca5 100644
--- a/src/components/search_bar/search_bar.js
+++ b/src/components/search_bar/search_bar.js
@@ -32,6 +32,7 @@ const SearchBar = {
this.$emit('toggled', this.hidden)
this.$nextTick(() => {
if (!this.hidden) {
+ this.searchTerm = undefined
this.$refs.searchInput.focus()
}
})
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 73413b48..d3bb40c3 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -1,4 +1,4 @@
-import { filter, trim } from 'lodash'
+import { filter, trim, debounce } from 'lodash'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
@@ -27,13 +27,13 @@ const FilteringTab = {
get () {
return this.muteWordsStringLocal
},
- set (value) {
+ set: debounce(function (value) {
this.muteWordsStringLocal = value
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
- }
+ }, 500)
}
},
// Updating nested properties
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 935591dc..6e31366b 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -105,8 +105,8 @@ const GeneralTab = {
return this.$store.getters.mergedConfig.profileVersion
},
translationLanguages () {
- const langs = this.$store.state.instance.translationLanguages || []
- return (langs || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
+ const langs = this.$store.state.instance.supportedTranslationLanguages || { source: [] }
+ return langs.source.map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
},
translationLanguage: {
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 8781bb91..b69cf2f1 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -151,7 +151,7 @@ const ProfileTab = {
return false
},
deleteField (index, event) {
- this.$delete(this.newFields, index)
+ this.newFields.splice(index, 1)
},
uploadFile (slot, e) {
const file = e.target.files[0]
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 1c8d5865..4e6e9edd 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -15,7 +15,8 @@ import {
faTachometerAlt,
faCog,
faInfoCircle,
- faList
+ faList,
+ faUserTie
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -30,7 +31,8 @@ library.add(
faTachometerAlt,
faCog,
faInfoCircle,
- faList
+ faList,
+ faUserTie
)
const SideDrawer = {
@@ -102,6 +104,9 @@ const SideDrawer = {
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
+ },
+ openModModal () {
+ this.$store.dispatch('openModModal')
}
}
}
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 6d78fa85..86943e27 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -143,6 +143,21 @@
/> {{ $t("nav.about") }}