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 @@ + diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 9ba5abc4..78e93f0e 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -16,7 +16,8 @@ import { faUsers, faCommentMedical, faBookmark, - faInfoCircle + faInfoCircle, + faUserTie } from '@fortawesome/free-solid-svg-icons' library.add( @@ -34,7 +35,8 @@ library.add( faUsers, faCommentMedical, faBookmark, - faInfoCircle + faInfoCircle, + faUserTie ) export default { @@ -109,6 +111,9 @@ export default { }, openSettingsModal () { this.$store.dispatch('openSettingsModal') + }, + openModModal () { + this.$store.dispatch('openModModal') } } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 9dc02e68..0c592326 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -151,6 +151,18 @@ :title="$t('nav.preferences')" /> + 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..d80205c9 --- /dev/null +++ b/src/components/mod_modal/mod_modal.scss @@ -0,0 +1,63 @@ +@import 'src/_variables.scss'; +.mod-modal { + overflow: hidden; + + .setting-list, + .option-list { + list-style-type: none; + padding-left: 2em; + li { + margin-bottom: 0.5em; + } + .suboptions { + margin-top: 0.3em + } + } + + &.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: 100%; + overflow-y: hidden; + + .btn { + min-height: 2em; + min-width: 10em; + padding: 0 2em; + } + } + } +} 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 @@ + + + + diff --git a/src/components/mod_modal/mod_modal_content.js b/src/components/mod_modal/mod_modal_content.js new file mode 100644 index 00000000..6330f162 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.js @@ -0,0 +1,63 @@ +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' + +import ReportsTab from './tabs/reports_tab.vue' +import StatusesTab from './tabs/statuses_tab.vue' +import UsersTab from './tabs/users_tab.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faFlag, + faMessage, + faUsers +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faFlag, + faMessage, + faUsers +) + +const ModModalContent = { + components: { + TabSwitcher, + + ReportsTab, + StatusesTab, + UsersTab + }, + computed: { + open () { + return this.$store.state.interface.modModalState !== 'hidden' + }, + bodyLock () { + return this.$store.state.interface.modModalState === 'visible' + } + }, + methods: { + onOpen () { + const targetTab = this.$store.state.interface.modModalTargetTab + // We're being told to open in specific tab + if (targetTab) { + const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => { + return elm.props && elm.props['data-tab-name'] === targetTab + }) + if (tabIndex >= 0) { + this.$refs.tabSwitcher.setTab(tabIndex) + } + } + // Clear the state of target tab, so that next time moderation is opened + // it doesn't force it. + this.$store.dispatch('clearModModalTargetTab') + } + }, + mounted () { + this.onOpen() + }, + watch: { + open: function (value) { + if (value) this.onOpen() + } + } +} + +export default ModModalContent diff --git a/src/components/mod_modal/mod_modal_content.scss b/src/components/mod_modal/mod_modal_content.scss new file mode 100644 index 00000000..99fc2d2a --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.scss @@ -0,0 +1,54 @@ +@import 'src/_variables.scss'; +.mod_tab-switcher { + height: 100%; + + .setting-item { + border-bottom: 2px solid var(--fg, $fallback--fg); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div, + > label { + display: block; + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + .select-multiple { + display: flex; + + .option-list { + margin: 0; + padding-left: .5em; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable svg { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + + .number-input { + max-width: 6em; + } + } +} diff --git a/src/components/mod_modal/mod_modal_content.vue b/src/components/mod_modal/mod_modal_content.vue new file mode 100644 index 00000000..bb31bc19 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.vue @@ -0,0 +1,34 @@ + + + + diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index c8d390bc..84fc14da 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -64,8 +64,12 @@ export default { settingsModalVisible () { return this.settingsModalState === 'visible' }, + modModalVisible () { + return this.modModalState === 'visible' + }, ...mapState({ - settingsModalState: state => state.interface.settingsModalState + settingsModalState: state => state.interface.settingsModalState, + modModalState: state => state.interface.modModalState }) }, beforeUpdate () { diff --git a/src/i18n/en.json b/src/i18n/en.json index e920cf11..2ef4ff5a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -266,6 +266,12 @@ "next": "Next", "previous": "Previous" }, + "moderation": { + "moderation": "Moderation", + "reports": "Reports", + "statuses": "Statuses", + "users": "Users" + }, "nav": { "about": "About", "administration": "Administration", diff --git a/src/modules/interface.js b/src/modules/interface.js index a86193ea..ae1a31c3 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -2,6 +2,9 @@ const defaultState = { settingsModalState: 'hidden', settingsModalLoaded: false, settingsModalTargetTab: null, + modModalState: 'hidden', + modModalLoaded: false, + modModalTargetTab: null, settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -63,6 +66,30 @@ const interfaceMod = { setSettingsModalTargetTab (state, value) { state.settingsModalTargetTab = value }, + closeModModal (state) { + state.modModalState = 'hidden' + }, + togglePeekModModal (state) { + switch (state.modModalState) { + case 'minimized': + state.modModalState = 'visible' + return + case 'visible': + state.modModalState = 'minimized' + return + default: + throw new Error('Illegal minimization state of mod modal') + } + }, + openModModal (state) { + state.modModalState = 'visible' + if (!state.modModalLoaded) { + state.modModalLoaded = true + } + }, + setModModalTargetTab (state, value) { + state.modModalTargetTab = value + }, pushGlobalNotice (state, notice) { state.globalNotices.push(notice) }, @@ -105,6 +132,18 @@ const interfaceMod = { commit('setSettingsModalTargetTab', value) commit('openSettingsModal') }, + closeModModal ({ commit }) { + commit('closeModModal') + }, + openModModal ({ commit }) { + commit('openModModal') + }, + togglePeekModModal ({ commit }) { + commit('togglePeekModModal') + }, + clearModModalTargetTab ({ commit }) { + commit('setModModalTargetTab', null) + }, pushGlobalNotice ( { commit, dispatch, state }, { diff --git a/yarn.lock b/yarn.lock index 86ae6b85..b8642176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1092,12 +1092,12 @@ "@fortawesome/fontawesome-common-types@^0.3.0": version "0.3.0" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893" integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w== "@fortawesome/fontawesome-svg-core@1.3.0": version "1.3.0" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885" integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg== dependencies: "@fortawesome/fontawesome-common-types" "^0.3.0" @@ -1141,9 +1141,9 @@ integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@intlify/bundle-utils@next": - version "3.1.2" - resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.1.2.tgz" - integrity sha512-amgSo0NN5OKWYdcgFmfJqo2tcUcZ6C66Bxm5ALQnB0m3MUQtS9aJzKoIo+EU9XQiOVmlBFxRtNoZm+psHa5FNA== + version "3.2.1" + resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.2.1.tgz" + integrity sha512-rf4cLBOnbqmpXVcCdcYHilZpMt1m82syh3WLBJlZvGxN2KkH9HeHVH4+bnibF/SDXCHNh6lM6wTpS/qw+PkcMg== dependencies: "@intlify/message-compiler" next "@intlify/shared" next @@ -1168,7 +1168,7 @@ dependencies: "@intlify/shared" "9.2.2" -"@intlify/message-compiler@9.2.2": +"@intlify/message-compiler@9.2.2", "@intlify/message-compiler@next": version "9.2.2" resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz" integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA== @@ -1176,24 +1176,11 @@ "@intlify/shared" "9.2.2" source-map "0.6.1" -"@intlify/message-compiler@next": - version "9.3.0-beta.3" - resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.3.tgz" - integrity sha512-j8OwToBQgs01RBMX4GCDNQfcnmw3AiDG3moKIONTrfXcf+1yt/rWznLTYH/DXbKcFMAFijFpCzMYjUmH1jVFYA== - dependencies: - "@intlify/shared" "9.3.0-beta.3" - source-map "0.6.1" - "@intlify/shared@9.2.2", "@intlify/shared@next": version "9.2.2" resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz" integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q== -"@intlify/shared@9.3.0-beta.3": - version "9.3.0-beta.3" - resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.3.tgz" - integrity sha512-Z/0TU4GhFKRxKh+0RbwJExik9zz57gXYgxSYaPn7YQdkQ/pabSioCY/SXnYxQHL6HzULF5tmqarFm6glbGqKhw== - "@intlify/vue-devtools@9.2.2": version "9.2.2" resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz" @@ -8095,9 +8082,9 @@ mute-stream@0.0.7: integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ== nan@^2.12.1: - version "2.16.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" - integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== nanoid@^3.3.4: version "3.3.4"