diff --git a/.eslintrc.js b/.eslintrc.js index 4868f360..28d39d03 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,17 +1,17 @@ module.exports = { root: true, parserOptions: { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', sourceType: 'module' }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: [ - 'standard', 'plugin:vue/recommended' ], // required to lint *.vue files plugins: [ - 'vue' + 'vue', + 'import' ], // add your custom rules here rules: { @@ -23,6 +23,8 @@ module.exports = { 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'vue/require-prop-types': 0, 'vue/no-unused-vars': 0, - 'no-tabs': 0 + 'no-tabs': 0, + 'vue/multi-word-component-names': 0, + 'vue/no-reserved-component-names': 0 } } diff --git a/.woodpecker.yml b/.woodpecker.yml index 7d97864b..b5b6e889 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -3,7 +3,7 @@ pipeline: when: event: - pull_request - image: node:16 + image: node:18 commands: - yarn - yarn lint @@ -13,7 +13,7 @@ pipeline: when: event: - pull_request - image: node:16 + image: node:18 commands: - apt update - apt install firefox-esr -y --no-install-recommends @@ -27,7 +27,7 @@ pipeline: branch: - develop - stable - image: node:16 + image: node:18 commands: - yarn - yarn build @@ -39,7 +39,7 @@ pipeline: branch: - develop - stable - image: node:16 + image: node:18 secrets: - SCW_ACCESS_KEY - SCW_SECRET_KEY diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..59961078 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,24 @@ +# Akkoma Code of Conduct + +The Akkoma project aims to be **enjoyable** for anyone to participate in, regardless of their identity or level of expertise. To achieve this, the community must create an environment which is **safe** and **equitable**; the following guidelines have been created with these goals in mind. + +1. **Treat individuals with respect.** Differing experiences and viewpoints deserve to be respected, and bigotry and harassment are not tolerated under any circumstances. + - Individuals should at all times be treated as equals, regardless of their age, gender, sexuality, race, ethnicity, _or any other characteristic_, intrinsic or otherwise. + - Behaviour that is harmful in nature should be addressed and corrected *regardless of intent*. + - Respect personal boundaries and ask for clarification whenever they are unclear. + - (Obviously, hate does not count as merely a "differing viewpoint", because it is harmful in nature.) + +2. **Be understanding of differences in communication.** Not everyone is aware of unspoken social cues, and speech that is not intended to be offensive should not be treated as such simply due to an atypical manner of communication. + - Somebody who speaks bluntly is not necessarily rude, and somebody who swears a lot is not necessarily volatile. + - Try to confirm your interpretation of their intent rather than assuming bad faith. + - Someone may not communicate as, or come across as a picture of "professionalism", but this should not be seen as a reason to dismiss them. This is a **casual** space, and communication styles can reflect that. + +3. **"Uncomfortable" does not mean "unsafe".** In an ideal world, the community would be safe, equitable, enjoyable, *and* comfortable for all members at all times. Unfortunately, this is not always possible in reality. + - Safety and equity will be prioritized over comfort whenever it is necessary to do so. + - Weaponizing one's own discomfort to deflect accountability or censor an individual (e.g. "white fragility") is a form of discriminatory conduct. + +4. **Let people grow from their mistakes.** Nobody is perfect; even the most well-meaning individual can do something hurtful. Everyone should be given a fair opportunity to explain themselves and correct their behaviour. Portraying someone as inherently malicious prevents improvement and shifts focus away from the *action* that was problematic. + - Avoid bringing up past events that do not accurately reflect an individual's current actions or beliefs. (This is, of course, different from providing evidence of a recurring pattern of behaviour.) + +--- +This document was adapted from one created by ~keith as part of punks default repository template, and is licensed under CC-BY-SA 4.0. The original template is here: diff --git a/COFE_OF_CONDUCT.md b/COFE_OF_CONDUCT.md deleted file mode 100644 index 1ba85c5f..00000000 --- a/COFE_OF_CONDUCT.md +++ /dev/null @@ -1,49 +0,0 @@ -``` - o$$$$$$oo - o$" "$oo - $ o""""$o "$o - "$ o "o "o $ - "$ $o $ $ o$ - "$ o$"$ o$ - "$ooooo$$ $ o$ - o$ """ $ " $$$ " $ - o$ $o $$" " " - $$ $ " $ $$$o"$ o o$" - $" o "" $ $" " o" $$ - $o " " $ o$" o" o$" - "$o $$ $ o" o$$" - ""o$o"$" $oo" o$" - o$$ $ $$$ o$$ - o" o oo"" "" "$o - o$o" "" $ - $" " o" " " " "o - $$ " " o$ o$o " $ - o$ $ $ o$$ " " "" - o $ $" " "o o$ - $ o $o$oo$"" - $o $ o o o"$$ - $o o $ $ "$o - $o $ o $ $ "o - $ $ "o $ "o"$o - $ " o $ o $$ - $o$o$o$o$$o$$$o$$o$o$$o$$o$$$o$o$o$o$o$o$o$o$o$ooo - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ " $$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$" - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ooooo$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""""" - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" -"$o$o$o$o$o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" - "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" - "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""" - """"""""""""""""""""""""""""""""""""""""""""""""""""" -``` \ No newline at end of file diff --git a/build/dev-server.js b/build/dev-server.js index 48574214..deacea62 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -28,18 +28,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, { }) var hotMiddleware = require('webpack-hot-middleware')(compiler) -// force page reload when html-webpack-plugin template changes -compiler.plugin('compilation', function (compilation) { - compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { - // FIXME: This supposed to reload whole page when index.html is changed, - // however now it reloads entire page on every breath, i suppose the order - // of plugins changed or something. It's a minor thing and douesn't hurt - // disabling it, constant reloads hurt much more - - // hotMiddleware.publish({ action: 'reload' }) - // cb() - }) -}) // proxy api requests Object.keys(proxyTable).forEach(function (context) { diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index e03848ac..e478d341 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -2,8 +2,8 @@ var path = require('path') var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') -var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') -var { VueLoaderPlugin } = require("vue-loader"); +const WorkboxPlugin = require('workbox-webpack-plugin'); +var { VueLoaderPlugin } = require('vue-loader') var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -19,6 +19,7 @@ module.exports = { app: './src/main.js' }, output: { + hashFunction: "sha256", // Workaround for builds with OpenSSL 3. path: config.build.assetsRoot, publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, filename: '[name].js' @@ -33,6 +34,9 @@ module.exports = { modules: [ path.join(__dirname, '../node_modules') ], + fallback: { + "url": require.resolve("url/"), + }, alias: { 'static': path.resolve(__dirname, '../static'), 'src': path.resolve(__dirname, '../src'), @@ -115,9 +119,10 @@ module.exports = { ] }, plugins: [ - new ServiceWorkerWebpackPlugin({ - entry: path.join(__dirname, '..', 'src/sw.js'), - filename: 'sw-pleroma.js' + new WorkboxPlugin.InjectManifest({ + swSrc: path.join(__dirname, '..', 'src/sw.js'), + swDest: 'sw-pleroma.js', + maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, }), new VueLoaderPlugin() ] diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js index 4605b93d..8c7ac5d7 100644 --- a/build/webpack.dev.conf.js +++ b/build/webpack.dev.conf.js @@ -1,6 +1,6 @@ var config = require('../config') var webpack = require('webpack') -var merge = require('webpack-merge') +var { merge } = require('webpack-merge') var utils = require('./utils') var baseWebpackConfig = require('./webpack.base.conf') var HtmlWebpackPlugin = require('html-webpack-plugin') @@ -16,7 +16,7 @@ module.exports = merge(baseWebpackConfig, { }, mode: 'development', // eval-source-map is faster for development - devtool: '#eval-source-map', + devtool: 'eval-source-map', plugins: [ new webpack.DefinePlugin({ 'process.env': config.dev.env, diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js index a67ed2f6..7c94ec3c 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -2,7 +2,7 @@ var path = require('path') var config = require('../config') var utils = require('./utils') var webpack = require('webpack') -var merge = require('webpack-merge') +var { merge } = require('webpack-merge') var baseWebpackConfig = require('./webpack.base.conf') var MiniCssExtractPlugin = require('mini-css-extract-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin') @@ -19,7 +19,7 @@ var webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true }) }, - devtool: config.build.productionSourceMap ? '#source-map' : false, + devtool: 'source-map', optimization: { minimize: true, splitChunks: { @@ -62,7 +62,7 @@ var webpackConfig = merge(baseWebpackConfig, { // https://github.com/kangax/html-minifier#options-quick-reference }, // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency' + chunksSortMode: 'auto' }), // split vendor js into its own file // extract webpack runtime and module manifest to its own file in order to diff --git a/config/dev.env.js b/config/dev.env.js index efead7c8..ae12ff34 100644 --- a/config/dev.env.js +++ b/config/dev.env.js @@ -1,4 +1,4 @@ -var merge = require('webpack-merge') +var { merge } = require('webpack-merge') var prodEnv = require('./prod.env') module.exports = merge(prodEnv, { diff --git a/config/test.env.js b/config/test.env.js index 89f90deb..646a6ef8 100644 --- a/config/test.env.js +++ b/config/test.env.js @@ -1,4 +1,4 @@ -var merge = require('webpack-merge') +var { merge } = require('webpack-merge') var devEnv = require('./dev.env') module.exports = merge(devEnv, { diff --git a/docs/docs/CONFIGURATION.md b/docs/docs/CONFIGURATION.md index cdcbc61a..d7b25802 100644 --- a/docs/docs/CONFIGURATION.md +++ b/docs/docs/CONFIGURATION.md @@ -70,9 +70,6 @@ Default post formatting option (markdown/bbcode/plaintext/etc...) ### `redirectRootNoLogin`, `redirectRootLogin` These two settings should point to where FE should redirect visitor when they login/open up website root -### `scopeCopy` -Copy post scope (visibility) when replying to a post. Instance-default. - ### `sidebarRight` Change alignment of sidebar and panels to the right. Defaults to `false`. diff --git a/index.html b/index.html index d81d2ed1..8ecfa323 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Pleroma + Akkoma @@ -13,7 +13,7 @@ - +
diff --git a/package.json b/package.json index 6712bf76..cd00c490 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,13 @@ "escape-html": "1.0.3", "js-cookie": "^3.0.1", "localforage": "1.10.0", - "marked": "^4.0.17", + "marked": "^4.2.2", "marked-mfm": "^0.5.0", - "parse-link-header": "1.0.1", + "parse-link-header": "^2.0.0", "phoenix": "1.6.2", "punycode.js": "2.1.0", "qrcode": "1", - "ruffle-mirror": "2021.12.31", + "url": "^0.11.0", "vue": "^3.2.31", "vue-i18n": "^9.2.2", "vue-router": "4.0.14", @@ -48,6 +48,7 @@ }, "devDependencies": { "@babel/core": "7.17.8", + "@babel/eslint-parser": "^7.19.1", "@babel/plugin-transform-runtime": "7.17.0", "@babel/preset-env": "7.16.11", "@babel/register": "7.17.7", @@ -58,31 +59,29 @@ "@vue/compiler-sfc": "^3.1.0", "@vue/test-utils": "^2.0.2", "autoprefixer": "6.7.7", - "babel-eslint": "7.2.3", - "babel-loader": "8.2.4", + "babel-loader": "^9.1.0", "babel-plugin-lodash": "3.3.4", - "chai": "3.5.0", + "chai": "^4.3.7", "chalk": "1.1.3", - "chromedriver": "87.0.7", - "connect-history-api-fallback": "1.6.0", - "copy-webpack-plugin": "6.4.1", - "cross-spawn": "4.0.2", - "css-loader": "0.28.11", - "custom-event-polyfill": "1.0.7", - "eslint": "5.16.0", - "eslint-config-standard": "12.0.0", - "eslint-friendly-formatter": "2.0.7", - "eslint-loader": "2.2.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-node": "7.0.1", - "eslint-plugin-promise": "4.3.1", - "eslint-plugin-standard": "4.1.0", - "eslint-plugin-vue": "5.2.3", + "chromedriver": "^107.0.3", + "connect-history-api-fallback": "^2.0.0", + "cross-spawn": "^7.0.3", + "css-loader": "^6.7.2", + "custom-event-polyfill": "^1.0.7", + "eslint": "^7.32.0", + "eslint-config-standard": "^17.0.0", + "eslint-friendly-formatter": "^4.0.1", + "eslint-loader": "^4.0.2", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-standard": "^5.0.0", + "eslint-plugin-vue": "^9.7.0", "eventsource-polyfill": "0.9.6", "express": "4.17.3", - "file-loader": "3.0.1", + "file-loader": "^6.2.0", "function-bind": "1.1.1", - "html-webpack-plugin": "3.2.0", + "html-webpack-plugin": "^5.5.0", "http-proxy-middleware": "0.21.0", "inject-loader": "2.0.1", "iso-639-1": "2.1.15", @@ -96,7 +95,7 @@ "karma-sinon-chai": "2.0.2", "karma-sourcemap-loader": "0.3.8", "karma-spec-reporter": "0.0.33", - "karma-webpack": "4.0.2", + "karma-webpack": "^5.0.0", "lodash": "4.17.21", "lolex": "1.6.0", "mini-css-extract-plugin": "0.12.0", @@ -106,27 +105,27 @@ "ora": "0.4.1", "postcss-loader": "3.0.0", "raw-loader": "0.5.1", - "sass": "1.53.0", - "sass-loader": "7.3.1", + "sass": "^1.56.0", + "sass-loader": "^13.2.0", "selenium-server": "2.53.1", "semver": "5.7.1", - "serviceworker-webpack-plugin": "1.0.1", "shelljs": "0.8.5", "sinon": "2.4.1", "sinon-chai": "2.14.0", "stylelint": "13.6.1", "stylelint-config-standard": "20.0.0", "stylelint-rscss": "0.4.0", - "url-loader": "1.1.2", - "vue-loader": "^16.0.0", - "vue-style-loader": "4.1.2", - "webpack": "4.46.0", - "webpack-dev-middleware": "3.7.3", - "webpack-hot-middleware": "2.25.1", - "webpack-merge": "0.20.0" + "url-loader": "^4.1.1", + "vue-loader": "^17.0.0", + "vue-style-loader": "^4.1.2", + "webpack": "^5.75.0", + "webpack-dev-middleware": "^5.3.3", + "webpack-hot-middleware": "^2.25.1", + "webpack-merge": "^5.8.0", + "workbox-webpack-plugin": "^6.5.4" }, "engines": { - "node": ">= 4.0.0", + "node": ">= 16.0.0", "npm": ">= 3.0.0" } } 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/boot/after_store.js b/src/boot/after_store.js index 38b19432..12919c2a 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -150,6 +150,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showWiderShortcuts') copyInstanceOption('showNavShortcuts') copyInstanceOption('showPanelNavShortcuts') + copyInstanceOption('stopGifs') copyInstanceOption('logo') store.dispatch('setInstanceOption', { @@ -171,10 +172,8 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('redirectRootNoLogin') copyInstanceOption('redirectRootLogin') copyInstanceOption('showInstanceSpecificPanel') - copyInstanceOption('minimalScopesMode') copyInstanceOption('hideMutedPosts') copyInstanceOption('collapseMessageWithSubject') - copyInstanceOption('scopeCopy') copyInstanceOption('subjectLineBehavior') copyInstanceOption('postContentType') copyInstanceOption('alwaysShowSubjectInput') @@ -398,6 +397,7 @@ const afterStoreSetup = async ({ store, i18n }) => { // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') store.dispatch('startFetchingAnnouncements') + store.dispatch('startFetchingReports') getTOS({ store }) getStickers({ store }) diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 1a0a90b1..1a35a77d 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -225,7 +225,7 @@ const Attachment = { this.$emit('resize', newHeight) }, postStatus (event) { - console.log(this.statusForm.postStatus(event, this.statusForm.newStatus)) + this.statusForm.postStatus(event, this.statusForm.newStatus) } } } diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index 2ef5cf4d..484ca0c4 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -26,6 +26,7 @@ display: flex; padding-top: 0.5em; z-index: 1; + max-height: 50%; p { flex: 1; @@ -36,7 +37,7 @@ white-space: pre-line; word-break: break-word; text-overflow: ellipsis; - overflow: hidden; + overflow: scroll; } &.-static { diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 9ba5abc4..f4900c38 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 { @@ -98,6 +100,9 @@ export default { privateMode () { return this.$store.state.instance.private }, shouldConfirmLogout () { return this.$store.getters.mergedConfig.modalOnLogout + }, + showBubbleTimeline () { + return this.$store.state.instance.localBubbleInstances.length > 0 } }, methods: { @@ -109,6 +114,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..92d3fa5b 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -55,7 +55,7 @@ /> @@ -151,6 +151,18 @@ :title="$t('nav.preferences')" /> + 0 || suggestion) { const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const replacement = chosenSuggestion.replacement diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 7d95ab7e..078253c2 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -42,7 +42,7 @@ :class="{ highlighted: index === highlighted }" @click.stop.prevent="onClick($event, suggestion)" > - + ({ 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/features_panel/features_panel.js b/src/components/features_panel/features_panel.js index 705d0502..e8823693 100644 --- a/src/components/features_panel/features_panel.js +++ b/src/components/features_panel/features_panel.js @@ -4,7 +4,6 @@ const FeaturesPanel = { computed: { whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, - minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode }, textlimit: function () { return this.$store.state.instance.textlimit }, uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } } 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 @@ + + + + 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..e0ba6259 --- /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/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..b1aeba38 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.scss @@ -0,0 +1,21 @@ +@import 'src/_variables.scss'; +.mod_tab-switcher { + height: 100%; + + .content { + margin: 1em 1em 1.4em; + + > div { + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + } +} 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..6fa32be1 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.vue @@ -0,0 +1,20 @@ + + + + diff --git a/src/components/mod_modal/tabs/reports_tab/report_card.js b/src/components/mod_modal/tabs/reports_tab/report_card.js new file mode 100644 index 00000000..6e6bfdae --- /dev/null +++ b/src/components/mod_modal/tabs/reports_tab/report_card.js @@ -0,0 +1,124 @@ +import Popover from 'src/components/popover/popover.vue' +import Status from 'src/components/status/status.vue' +import UserAvatar from 'src/components/user_avatar/user_avatar.vue' +import ReportNote from './report_note.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faChevronUp +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faChevronUp +) + +const FORCE_NSFW = 'mrf_tag:media-force-nsfw' +const STRIP_MEDIA = 'mrf_tag:media-strip' +const FORCE_UNLISTED = 'mrf_tag:force-unlisted' +const SANDBOX = 'mrf_tag:sandbox' + +const ReportCard = { + data () { + return { + hidden: true, + statusesHidden: true, + notesHidden: true, + note: null, + tags: { + FORCE_NSFW, + STRIP_MEDIA, + FORCE_UNLISTED, + SANDBOX + } + } + }, + props: [ + 'account', + 'actor', + 'content', + 'id', + 'notes', + 'state', + 'statuses' + ], + components: { + ReportNote, + Popover, + Status, + UserAvatar + }, + created () { + this.$store.dispatch('fetchUser', this.account.id) + }, + computed: { + isOpen () { + return this.state === 'open' + }, + tagPolicyEnabled () { + return this.$store.state.instance.federationPolicy.mrf_policies.includes('TagPolicy') + }, + user () { + return this.$store.getters.findUser(this.account.id) + } + }, + methods: { + toggleHidden () { + this.hidden = !this.hidden + }, + decode (content) { + content = content.replaceAll('
', '\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..e77afa79 --- /dev/null +++ b/src/components/mod_modal/tabs/reports_tab/report_card.vue @@ -0,0 +1,202 @@ +