diff --git a/.babelrc b/.babelrc index 945211470b..373d2c5993 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], - "plugins": ["@babel/plugin-transform-runtime", "lodash"], + "presets": ["@babel/preset-env"], + "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], "comments": false } diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85d3ee4466..ab60117331 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official framework image. Look for the different tagged releases at: # https://hub.docker.com/r/library/node/tags/ -image: node:10 +image: node:12 stages: - lint diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cc6994cd..fac68ac04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Media modal now also displays description and counter position in gallery (i.e. 1/5) - Ability to rearrange order of attachments when uploading - Enabled users to zoom and pan images in media viewer with mouse and touch +- Added frontend ui for account migration ## [2.4.2] - 2022-01-09 diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 900d824b98..f8c544d702 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -4,6 +4,7 @@ var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') var CopyPlugin = require('copy-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 @@ -29,16 +30,16 @@ module.exports = { } }, resolve: { - extensions: ['.js', '.vue'], + extensions: ['.js', '.jsx', '.vue'], modules: [ path.join(__dirname, '../node_modules') ], alias: { - 'vue$': 'vue/dist/vue.runtime.common', 'static': path.resolve(__dirname, '../static'), 'src': path.resolve(__dirname, '../src'), 'assets': path.resolve(__dirname, '../src/assets'), - 'components': path.resolve(__dirname, '../src/components') + 'components': path.resolve(__dirname, '../src/components'), + 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' } }, module: { @@ -58,9 +59,28 @@ module.exports = { } } }, + { + enforce: 'post', + test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files + type: 'javascript/auto', + loader: '@intlify/vue-i18n-loader', + include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled + path.resolve(__dirname, '../src/i18n') + ] + }, { test: /\.vue$/, - use: 'vue-loader' + loader: 'vue-loader', + options: { + compilerOptions: { + isCustomElement(tag) { + if (tag === 'pinch-zoom') { + return true + } + return false + } + } + } }, { test: /\.jsx?$/, @@ -95,6 +115,7 @@ module.exports = { entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' }), + new VueLoaderPlugin(), // This copies Ruffle's WASM to a directory so that JS side can access it new CopyPlugin({ patterns: [ diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js index 159572ba1f..4605b93dfb 100644 --- a/build/webpack.dev.conf.js +++ b/build/webpack.dev.conf.js @@ -21,7 +21,9 @@ module.exports = merge(baseWebpackConfig, { new webpack.DefinePlugin({ 'process.env': config.dev.env, 'COMMIT_HASH': JSON.stringify('DEV'), - 'DEV_OVERRIDES': JSON.stringify(config.dev.settings) + 'DEV_OVERRIDES': JSON.stringify(config.dev.settings), + '__VUE_OPTIONS_API__': true, + '__VUE_PROD_DEVTOOLS__': false }), // https://github.com/glenjamin/webpack-hot-middleware#installation--usage new webpack.HotModuleReplacementPlugin(), diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js index ed11ebad35..a67ed2f627 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -36,7 +36,9 @@ var webpackConfig = merge(baseWebpackConfig, { new webpack.DefinePlugin({ 'process.env': env, 'COMMIT_HASH': JSON.stringify(commitHash), - 'DEV_OVERRIDES': JSON.stringify(undefined) + 'DEV_OVERRIDES': JSON.stringify(undefined), + '__VUE_OPTIONS_API__': true, + '__VUE_PROD_DEVTOOLS__': false }), // extract css into its own file new MiniCssExtractPlugin({ diff --git a/config/index.js b/config/index.js index 7cb87c3b36..023d4c9bce 100644 --- a/config/index.js +++ b/config/index.js @@ -52,7 +52,10 @@ module.exports = { target, changeOrigin: true, cookieDomainRewrite: 'localhost', - ws: true + ws: true, + headers: { + 'Origin': target + } }, '/oauth/revoke': { target, diff --git a/package.json b/package.json index 1b01a9856a..f9c0e436f1 100644 --- a/package.json +++ b/package.json @@ -16,44 +16,49 @@ "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "7.17.7", - "@chenfengyuan/vue-qrcode": "1.0.2", + "@babel/runtime": "7.18.6", + "@chenfengyuan/vue-qrcode": "2.0.0", "@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", - "@fortawesome/vue-fontawesome": "2.0.6", + "@fortawesome/vue-fontawesome": "3.0.1", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", + "@vuelidate/core": "2.0.0-alpha.42", + "@vuelidate/validators": "2.0.0-alpha.30", "body-scroll-lock": "2.7.1", "chromatism": "3.0.0", + "click-outside-vue3": "4.0.1", "cropperjs": "1.5.12", "diff": "3.5.0", "escape-html": "1.0.3", - "localforage": "1.7.3", + "js-cookie": "^3.0.1", + "localforage": "1.10.0", "parse-link-header": "1.0.1", - "phoenix": "1.4.0", - "portal-vue": "2.1.7", + "phoenix": "1.6.2", "punycode.js": "2.1.0", - "ruffle-mirror": "2021.4.11", - "v-click-outside": "2.1.5", - "vue": "2.6.11", - "vue-i18n": "7.8.1", - "vue-router": "3.0.2", + "qrcode": "1", + "ruffle-mirror": "2021.12.31", + "utf8": "^3.0.0", + "vue": "^3.2.31", + "vue-i18n": "^9.2.0-beta.34", + "vue-router": "4.0.14", "vue-template-compiler": "2.6.11", - "vuelidate": "0.7.7", - "vuex": "3.0.1" + "vuex": "4.0.2" }, "devDependencies": { - "@babel/core": "7.17.7", - "@babel/plugin-transform-runtime": "7.17.0", - "@babel/preset-env": "7.16.11", - "@babel/register": "7.17.7", + "@babel/core": "7.18.6", + "@babel/plugin-transform-runtime": "7.18.6", + "@babel/preset-env": "7.18.6", + "@babel/register": "7.18.6", + "@intlify/vue-i18n-loader": "^5.0.0", "@ungap/event-target": "0.2.3", "@vue/babel-helper-vue-jsx-merge-props": "1.2.1", - "@vue/babel-preset-jsx": "1.2.4", - "@vue/test-utils": "1.0.0-beta.28", + "@vue/babel-plugin-jsx": "1.1.1", + "@vue/compiler-sfc": "^3.1.0", + "@vue/test-utils": "2.0.0-rc.17", "autoprefixer": "6.7.7", "babel-eslint": "7.2.3", - "babel-loader": "8.2.3", + "babel-loader": "8.2.5", "babel-plugin-lodash": "3.3.4", "chai": "3.5.0", "chalk": "1.1.3", @@ -66,58 +71,58 @@ "eslint": "5.16.0", "eslint-config-standard": "12.0.0", "eslint-friendly-formatter": "2.0.7", - "eslint-loader": "2.1.2", - "eslint-plugin-import": "2.17.2", + "eslint-loader": "2.2.1", + "eslint-plugin-import": "2.26.0", "eslint-plugin-node": "7.0.1", - "eslint-plugin-promise": "4.1.1", - "eslint-plugin-standard": "4.0.0", + "eslint-plugin-promise": "4.3.1", + "eslint-plugin-standard": "4.1.0", "eslint-plugin-vue": "5.2.3", "eventsource-polyfill": "0.9.6", - "express": "4.16.4", + "express": "4.18.1", "file-loader": "3.0.1", "function-bind": "1.1.1", "html-webpack-plugin": "3.2.0", - "http-proxy-middleware": "0.17.4", + "http-proxy-middleware": "0.21.0", "inject-loader": "2.0.1", - "iso-639-1": "2.0.3", + "iso-639-1": "2.1.15", "isparta-loader": "2.0.0", "json-loader": "0.5.7", - "karma": "3.1.4", + "karma": "6.4.0", "karma-coverage": "1.1.2", - "karma-firefox-launcher": "1.1.0", - "karma-mocha": "1.3.0", + "karma-firefox-launcher": "1.3.0", + "karma-mocha": "2.0.1", "karma-mocha-reporter": "2.2.5", "karma-sinon-chai": "2.0.2", "karma-sourcemap-loader": "0.3.8", - "karma-spec-reporter": "0.0.33", + "karma-spec-reporter": "0.0.34", "karma-webpack": "4.0.2", "lodash": "4.17.21", "lolex": "1.6.0", - "mini-css-extract-plugin": "0.5.0", + "mini-css-extract-plugin": "0.12.0", "mocha": "3.5.3", "nightwatch": "0.9.21", "opn": "4.0.2", - "ora": "0.3.0", + "ora": "0.4.1", "postcss-loader": "3.0.0", "raw-loader": "0.5.1", - "sass": "1.20.1", - "sass-loader": "7.2.0", + "sass": "1.53.0", + "sass-loader": "7.3.1", "selenium-server": "2.53.1", - "semver": "5.6.0", + "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": "13.13.1", "stylelint-config-standard": "20.0.0", "stylelint-rscss": "0.4.0", "url-loader": "1.1.2", - "vue-loader": "14.2.4", + "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.24.3", - "webpack-merge": "0.14.1" + "webpack-hot-middleware": "2.25.1", + "webpack-merge": "0.20.0" }, "engines": { "node": ">= 4.0.0", diff --git a/src/App.js b/src/App.js index f5e0b9e9b6..f01f878826 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,5 @@ import UserPanel from './components/user_panel/user_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue' -import Notifications from './components/notifications/notifications.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' @@ -16,13 +15,14 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' +import { defineAsyncComponent } from 'vue' export default { name: 'app', components: { UserPanel, NavPanel, - Notifications, + Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')), InstanceSpecificPanel, FeaturesPanel, WhoToFollowPanel, @@ -46,10 +46,20 @@ export default { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) window.addEventListener('resize', this.updateMobileState) }, - destroyed () { + unmounted () { window.removeEventListener('resize', this.updateMobileState) }, computed: { + classes () { + return [ + { + '-reverse': this.reverseLayout, + '-no-sticky-headers': this.noSticky, + '-has-new-post-button': this.newPostButtonShown + }, + '-' + this.layoutType + ] + }, currentUser () { return this.$store.state.users.currentUser }, userBackground () { return this.currentUser.background_image }, instanceBackground () { @@ -65,38 +75,45 @@ export default { } } }, - shout () { return this.$store.state.shout.channel.state === 'joined' }, + shout () { return this.$store.state.shout.joined }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && !this.$store.getters.mergedConfig.hideISP && this.$store.state.instance.instanceSpecificPanelContent }, + isChats () { + return this.$route.name === 'chat' || this.$route.name === 'chats' + }, + newPostButtonShown () { + if (this.isChats) return false + return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' + }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, shoutboxPosition () { - return this.$store.getters.mergedConfig.showNewPostButton || false + return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false }, hideShoutbox () { return this.$store.getters.mergedConfig.hideShoutbox }, - isMobileLayout () { return this.$store.state.interface.mobileLayout }, + layoutType () { return this.$store.state.interface.layoutType }, privateMode () { return this.$store.state.instance.private }, - sidebarAlign () { - return { - 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 + reverseLayout () { + const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig + if (this.layoutType !== 'wide') { + return reverseSetting + } else { + return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting } }, + noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, + showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, ...mapGetters(['mergedConfig']) }, methods: { updateMobileState () { - const mobileLayout = windowWidth() <= 800 - const layoutHeight = windowHeight() - const changed = mobileLayout !== this.isMobileLayout - if (changed) { - this.$store.dispatch('setMobileLayout', mobileLayout) - } - this.$store.dispatch('setLayoutHeight', layoutHeight) + this.$store.dispatch('setLayoutWidth', windowWidth()) + this.$store.dispatch('setLayoutHeight', windowHeight()) } } } diff --git a/src/App.scss b/src/App.scss index bc027f4fca..7e6d0dfc55 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,77 +1,332 @@ +// stylelint-disable rscss/class-format @import './_variables.scss'; -#app { - min-height: 100vh; - max-width: 100%; - overflow: hidden; -} - -.app-bg-wrapper { - position: fixed; - z-index: -1; - height: 100%; - left: 0; - right: -20px; - background-size: cover; - background-repeat: no-repeat; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-position: 50% 50px; -} - -i[class^='icon-'] { - user-select: none; -} - -h4 { - margin: 0; -} - -#content { - box-sizing: border-box; - padding-top: 60px; - margin: auto; - min-height: 100vh; - max-width: 980px; - align-content: flex-start; -} - -.underlay { - background-color: rgba(0,0,0,0.15); - background-color: var(--underlay, rgba(0,0,0,0.15)); -} - -.text-center { - text-align: center; +:root { + --navbar-height: 3.5rem; + --post-line-height: 1.4; } html { font-size: 14px; + // overflow-x: clip causes my browser's tab to crash with SIGILL lul } body { - overscroll-behavior-y: none; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; color: $fallback--text; color: var(--text, $fallback--text); - max-width: 100vw; - overflow-x: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + overscroll-behavior-y: none; + overflow-x: clip; + overflow-y: scroll; &.hidden { display: none; } } +// ## Custom scrollbars +// Only show custom scrollbars on devices which +// have a cursor/pointer to operate them +@media (any-pointer: fine) { + * { + scrollbar-color: var(--btn) transparent; + + &::-webkit-scrollbar { + background: transparent; + } + + &::-webkit-scrollbar-button, + &::-webkit-scrollbar-thumb { + background-color: var(--btn); + box-shadow: var(--buttonShadow); + border-radius: var(--btnRadius); + } + + // horizontal/vertical/increment/decrement are webkit-specific stuff + // that indicates whether we're affecting vertical scrollbar, increase button etc + // stylelint-disable selector-pseudo-class-no-unknown + &::-webkit-scrollbar-button { + --___bgPadding: 2px; + + color: var(--btnText); + background-repeat: no-repeat, no-repeat; + + &:horizontal { + background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding)); + + &:increment { + background-image: + linear-gradient(45deg, var(--btnText) 50%, transparent 51%), + linear-gradient(-45deg, transparent 50%, var(--btnText) 51%); + background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding); + } + + &:decrement { + background-image: + linear-gradient(45deg, transparent 50%, var(--btnText) 51%), + linear-gradient(-45deg, var(--btnText) 50%, transparent 51%); + background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding); + } + } + + &:vertical { + background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%; + + &:increment { + background-image: + linear-gradient(-45deg, transparent 50%, var(--btnText) 51%), + linear-gradient(45deg, transparent 50%, var(--btnText) 51%); + background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%; + } + + &:decrement { + background-image: + linear-gradient(-45deg, var(--btnText) 50%, transparent 51%), + linear-gradient(45deg, var(--btnText) 50%, transparent 51%); + background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%; + } + } + } + // stylelint-enable selector-pseudo-class-no-unknown + } + // Body should have background to scrollbar otherwise it will use white (body color?) + html { + scrollbar-color: var(--selectedMenu) var(--wallpaper); + background: var(--wallpaper); + } +} + a { text-decoration: none; color: $fallback--link; color: var(--link, $fallback--link); } +h4 { + margin: 0; +} + +i[class*=icon-], +.svg-inline--fa { + color: $fallback--icon; + color: var(--icon, $fallback--icon); +} + +nav { + z-index: 1000; + color: var(--topBarText); + background-color: $fallback--fg; + background-color: var(--topBar, $fallback--fg); + color: $fallback--faint; + color: var(--faint, $fallback--faint); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); + box-shadow: var(--topBarShadow); + box-sizing: border-box; + height: var(--navbar-height); + position: fixed; +} + +#sidebar { + grid-area: sidebar; +} + +.column.-scrollable { + top: var(--navbar-height); + position: sticky; +} + +#main-scroller { + grid-area: content; + position: relative; +} + +#notifs-column { + grid-area: notifs; +} + +.app-bg-wrapper { + position: fixed; + height: 100%; + top: var(--navbar-height); + z-index: -1000; + left: 0; + right: -20px; + background-size: cover; + background-repeat: no-repeat; + background-color: var(--wallpaper); + background-image: var(--body-background-image); + background-position: 50%; +} + +.underlay { + grid-column-start: 1; + grid-column-end: span 3; + grid-row-start: 1; + grid-row-end: 1; + pointer-events: none; + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--underlay, rgba(0, 0, 0, 0.15)); + z-index: -1000; +} + +.app-layout { + --miniColumn: 25rem; + --maxiColumn: minmax(var(--miniColumn), 45rem); + --columnGap: 1em; + --status-margin: 0.75em; + + position: relative; + display: grid; + grid-template-columns: var(--miniColumn) var(--maxiColumn); + grid-template-areas: "sidebar content"; + grid-template-rows: 1fr; + box-sizing: border-box; + margin: 0 auto; + align-content: flex-start; + flex-wrap: wrap; + justify-content: center; + min-height: 100vh; + overflow-x: clip; + + .column { + --___columnMargin: var(--columnGap); + + display: grid; + grid-template-columns: 100%; + box-sizing: border-box; + grid-row-start: 1; + grid-row-end: 1; + margin: 0 calc(var(--___columnMargin) / 2); + padding: calc(var(--___columnMargin)) 0; + row-gap: var(--___columnMargin); + align-content: start; + + &:not(.-scrollable) { + margin-top: var(--navbar-height); + } + + &:hover { + z-index: 2; + } + + &.-full-height { + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + } + + &.-scrollable { + --___paddingIncrease: calc(var(--columnGap) / 2); + + position: sticky; + top: var(--navbar-height); + max-height: calc(100vh - var(--navbar-height)); + overflow-y: auto; + overflow-x: hidden; + margin-left: calc(var(--___paddingIncrease) * -1); + padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); + + // On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode + // might implement old style of hiding scrollbars later if there's demand + @supports (scrollbar-width: none) or (-webkit-text-fill-color: initial) { + &:not(.-show-scrollbar) { + scrollbar-width: none; + margin-right: calc(var(--___paddingIncrease) * -1); + padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); + + &::-webkit-scrollbar { + display: block; + width: 0; + } + } + } + + .panel-heading.-sticky { + top: calc(var(--columnGap) / -1); + } + } + } + + &.-has-new-post-button { + .column { + padding-bottom: 10rem; + } + } + + &.-no-sticky-headers { + .column { + .panel-heading.-sticky { + position: relative; + top: 0; + } + } + } + + .column-inner { + display: grid; + grid-template-columns: 100%; + box-sizing: border-box; + row-gap: 1em; + align-content: start; + } + + &.-reverse:not(.-wide):not(.-mobile) { + grid-template-columns: var(--maxiColumn) var(--miniColumn); + grid-template-areas: "content sidebar"; + } + + &.-wide { + grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn); + grid-template-areas: "sidebar content notifs"; + + &.-reverse { + grid-template-areas: "notifs content sidebar"; + } + } + + &.-mobile { + grid-template-columns: 100vw; + grid-template-areas: "content"; + padding: 0; + + .column { + margin-left: 0; + margin-right: 0; + padding-top: 0; + margin-top: var(--navbar-height); + margin-bottom: 0; + } + + .panel-heading, + .panel-heading::after, + .panel-heading::before, + .panel, + .panel::after { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + #sidebar, + #notifs-column { + display: none; + } + } + + &.-normal { + #notifs-column { + display: none; + } + } +} + +.text-center { + text-align: center; +} + .button-default { user-select: none; color: $fallback--text; @@ -84,7 +339,7 @@ a { cursor: pointer; box-shadow: $fallback--buttonShadow; box-shadow: var(--buttonShadow); - font-size: 14px; + font-size: 1em; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); @@ -103,12 +358,12 @@ a { } &:hover { - box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3); + box-shadow: 0 0 4px rgba(255, 255, 255, 0.3); box-shadow: var(--buttonHoverShadow); } &:active { - box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; + box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: var(--buttonPressedShadow); color: $fallback--text; color: var(--btnPressedText, $fallback--text); @@ -141,7 +396,7 @@ a { color: var(--btnToggledText, $fallback--text); background-color: $fallback--fg; background-color: var(--btnToggled, $fallback--fg); - box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; + box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: var(--buttonPressedShadow); svg, @@ -191,8 +446,9 @@ a { } } -input, textarea, .input { - +input, +textarea, +.input { &.unstyled { border-radius: 0; background: none; @@ -200,10 +456,12 @@ input, textarea, .input { height: unset; } + --_padding: 0.5em; + border: none; border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset; box-shadow: var(--inputShadow); background-color: $fallback--fg; background-color: var(--input, $fallback--fg); @@ -211,17 +469,18 @@ input, textarea, .input { color: var(--inputText, $fallback--lightText); font-family: sans-serif; font-family: var(--inputFont, sans-serif); - font-size: 14px; + font-size: 1em; margin: 0; box-sizing: border-box; display: inline-block; position: relative; - height: 28px; - line-height: 16px; + line-height: 2; hyphens: none; - padding: 8px .5em; + padding: 0 var(--_padding); - &:disabled, &[disabled=disabled], &.disabled { + &:disabled, + &[disabled=disabled], + &.disabled { cursor: not-allowed; opacity: 0.5; } @@ -236,18 +495,21 @@ input, textarea, .input { &[type=radio] { display: none; + &:checked + label::before { - box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset; - box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset; + box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset; + box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset; background-color: var(--accent, $fallback--link); } + &:disabled { &, & + label, & + label::before { - opacity: .5; + opacity: 0.5; } } + + label::before { flex-shrink: 0; display: inline-block; @@ -256,35 +518,37 @@ input, textarea, .input { width: 1.1em; height: 1.1em; border-radius: 100%; // Radio buttons should always be circle - box-shadow: 0px 0px 2px black inset; + box-shadow: 0 0 2px black inset; box-shadow: var(--inputShadow); - margin-right: .5em; + margin-right: 0.5em; background-color: $fallback--fg; background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; - line-height: 1.1em; + line-height: 1.1; font-size: 1.1em; box-sizing: border-box; color: transparent; overflow: hidden; - box-sizing: border-box; } } &[type=checkbox] { display: none; + &:checked + label::before { color: $fallback--text; color: var(--inputText, $fallback--text); } + &:disabled { &, & + label, & + label::before { - opacity: .5; + opacity: 0.5; } } + + label::before { flex-shrink: 0; display: inline-block; @@ -294,19 +558,18 @@ input, textarea, .input { height: 1.1em; border-radius: $fallback--checkboxRadius; border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0px 0px 2px black inset; + box-shadow: 0 0 2px black inset; box-shadow: var(--inputShadow); - margin-right: .5em; + margin-right: 0.5em; background-color: $fallback--fg; background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; - line-height: 1.1em; + line-height: 1.1; font-size: 1.1em; box-sizing: border-box; color: transparent; overflow: hidden; - box-sizing: border-box; } } @@ -315,6 +578,12 @@ input, textarea, .input { } } +// Textareas should have stock line-height + vertical padding instead of huge line-height +textarea { + padding: var(--_padding); + line-height: var(--post-line-height); +} + option { color: $fallback--text; color: var(--text, $fallback--text); @@ -324,6 +593,7 @@ option { .hide-number-spinner { -moz-appearance: textfield; + &[type=number]::-webkit-inner-spin-button, &[type=number]::-webkit-outer-spin-button { opacity: 0; @@ -331,11 +601,6 @@ option { } } -i[class*=icon-], .svg-inline--fa { - color: $fallback--icon; - color: var(--icon, $fallback--icon); -} - .btn-block { display: block; width: 100%; @@ -362,273 +627,16 @@ i[class*=icon-], .svg-inline--fa { } } -.container { - display: flex; - flex-wrap: wrap; - margin: 0; - padding: 0 10px 0 10px; -} - -.auto-size { - flex: 1 -} - -main-router { - flex: 1; -} - -.status.compact { - color: rgba(0, 0, 0, 0.42); - font-weight: 300; - - p { - margin: 0; - font-size: 0.8em - } -} - -/* Panel */ - -.panel { - display: flex; - position: relative; - - flex-direction: column; - margin: 0.5em; - - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - - &::after, & { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); - } - - &::after { - content: ''; - position: absolute; - - top: 0; - bottom: 0; - left: 0; - right: 0; - - pointer-events: none; - - box-shadow: 1px 1px 4px rgba(0,0,0,.6); - box-shadow: var(--panelShadow); - } -} - -.panel-body:empty::before { - content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations - display: block; - margin: 1em; - text-align: center; -} - -.panel-heading { - display: flex; - flex: none; - border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; - border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; - background-size: cover; - padding: .6em .6em; - text-align: left; - line-height: 28px; - color: var(--panelText); - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); - align-items: baseline; - box-shadow: var(--panelHeaderShadow); - - .title { - flex: 1 0 auto; - font-size: 1.3em; - } - - .faint { - background-color: transparent; - color: $fallback--faint; - color: var(--panelFaint, $fallback--faint); - } - - .faint-link { - color: $fallback--faint; - color: var(--faintLink, $fallback--faint); - } - - .alert { - white-space: nowrap; - text-overflow: ellipsis; - overflow-x: hidden; - } - - .button-default, - .alert { - // height: 100%; - line-height: 21px; - min-height: 0; - box-sizing: border-box; - margin: 0; - margin-left: .5em; - min-width: 1px; - align-self: stretch; - } - - .button-default { - flex-shrink: 0; - - &, - i[class*=icon-] { - color: $fallback--text; - color: var(--btnPanelText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedPanel, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedPanelText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledPanelText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledPanelText, $fallback--text); - } - } - - a, - .-link { - color: $fallback--link; - color: var(--panelLink, $fallback--link) - } -} - -.panel-heading.stub { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); -} - -/* TODO Should remove timeline-footer from here when we refactor panels into - * separate component and utilize slots - */ -.panel-footer, .timeline-footer { - display: flex; - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - flex: none; - padding: 0.6em 0.6em; - text-align: left; - line-height: 28px; - align-items: baseline; - border-width: 1px 0 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - - .faint { - color: $fallback--faint; - color: var(--panelFaint, $fallback--faint); - } - - a, - .-link { - color: $fallback--link; - color: var(--panelLink, $fallback--link); - } -} - -.panel-body > p { - line-height: 18px; - padding: 1em; - margin: 0; -} - -.container > * { - min-width: 0px; -} +@import './panel.scss'; .fa { color: grey; } -nav { - z-index: 1000; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - color: $fallback--faint; - color: var(--faint, $fallback--faint); - box-shadow: 0px 0px 4px rgba(0,0,0,.6); - box-shadow: var(--topBarShadow); - box-sizing: border-box; -} - -.fade-enter-active, .fade-leave-active { - transition: opacity .2s -} -.fade-enter, .fade-leave-active { - opacity: 0 -} - -.main { - flex-basis: 50%; - flex-grow: 1; - flex-shrink: 1; -} - -.sidebar-bounds { - flex: 0; - flex-basis: 35%; -} - -.sidebar-flexer { - flex: 1; - flex-basis: 345px; - width: 365px; -} - .mobile-shown { display: none; } -@media all and (min-width: 800px) { - body { - overflow-y: scroll; - } - - .sidebar-bounds { - overflow: hidden; - max-height: 100vh; - width: 345px; - position: fixed; - margin-top: -10px; - - .sidebar-scroller { - height: 96vh; - width: 365px; - padding-top: 10px; - padding-right: 50px; - overflow-x: hidden; - overflow-y: scroll; - } - - .sidebar { - width: 345px; - } - } - .sidebar-flexer { - max-height: 96vh; - flex-shrink: 0; - flex-grow: 0; - } -} - .badge { box-sizing: border-box; display: inline-block; @@ -656,12 +664,10 @@ nav { } .alert { - margin: 0.35em; - padding: 0.25em; + margin: 0 0.35em; + padding: 0 0.25em; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - min-height: 28px; - line-height: 28px; &.error { background-color: $fallback--alertError; @@ -712,7 +718,7 @@ nav { } .visibility-notice { - padding: .5em; + padding: 0.5em; border: 1px solid $fallback--faint; border: 1px solid var(--faint, $fallback--faint); border-radius: $fallback--inputRadius; @@ -727,7 +733,7 @@ nav { position: absolute; top: 0; right: 0; - padding: .5em; + padding: 0.5em; color: inherit; } } @@ -744,72 +750,6 @@ nav { } } -@keyframes shakeError { - 0% { - transform: translateX(0); - } - 15% { - transform: translateX(0.375rem); - } - 30% { - transform: translateX(-0.375rem); - } - 45% { - transform: translateX(0.375rem); - } - 60% { - transform: translateX(-0.375rem); - } - 75% { - transform: translateX(0.375rem); - } - 90% { - transform: translateX(-0.375rem); - } - 100% { - transform: translateX(0); - } -} - -@media all and (max-width: 800px) { - .mobile-hidden { - display: none; - } - - .panel-switcher { - display: flex; - } - - .container { - padding: 0; - } - - .panel { - margin: 0.5em 0 0.5em 0; - } - - .menu-button { - display: block; - margin-right: 0.8em; - } - - .main { - margin-bottom: 7em; - } -} - -.setting-list, -.option-list{ - list-style-type: none; - padding-left: 2em; - li { - margin-bottom: 0.5em; - } - .suboptions { - margin-top: 0.3em - } -} - .login-hint { text-align: center; @@ -819,18 +759,26 @@ nav { a { display: inline-block; - padding: 1em 0px; + padding: 1em 0; width: 100%; } } .btn.button-default { - min-height: 28px; + min-height: 2em; } -.animate-spin { - animation: spin 2s infinite linear; - display: inline-block; +.new-status-notification { + position: relative; + font-size: 1.1em; + z-index: 1; + flex: 1; +} + +@media all and (max-width: 800px) { + .mobile-hidden { + display: none; + } } @keyframes spin { @@ -843,49 +791,47 @@ nav { } } -.new-status-notification { - position: relative; - font-size: 1.1em; - z-index: 1; - flex: 1; -} - -.chat-layout { - // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). - overflow: hidden; - height: 100%; - - // Get rid of scrollbar on body as scrolling happens on different element - body { - overflow: hidden; +@keyframes shakeError { + 0% { + transform: translateX(0); } - // Ensures the fixed position of the mobile browser bars on scroll up / down events. - // Prevents the mobile browser bars from overlapping or hiding the message posting form. - @media all and (max-width: 800px) { - body { - height: 100%; - } + 15% { + transform: translateX(0.375rem); + } - #app { - height: 100%; - overflow: hidden; - min-height: auto; - } + 30% { + transform: translateX(-0.375rem); + } - #app_bg_wrapper { - overflow: hidden; - } + 45% { + transform: translateX(0.375rem); + } - .main { - overflow: hidden; - height: 100%; - } + 60% { + transform: translateX(-0.375rem); + } - #content { - padding-top: 0; - height: 100%; - overflow: visible; - } + 75% { + transform: translateX(0.375rem); + } + + 90% { + transform: translateX(-0.375rem); + } + + 100% { + transform: translateX(0); } } + +// Vue transitions +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.2s; +} + +.fade-enter-from, +.fade-leave-active { + opacity: 0; +} diff --git a/src/App.vue b/src/App.vue index eb65b548d8..21f6f6868c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,39 +1,32 @@ diff --git a/src/boot/after_store.js b/src/boot/after_store.js index c4a0a80099..f655c38f04 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,8 +1,14 @@ -import Vue from 'vue' -import VueRouter from 'vue-router' -import routes from './routes' +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import vClickOutside from 'click-outside-vue3' + +import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' + import App from '../App.vue' -import { windowWidth } from '../services/window_utils/window_utils' +import routes from './routes' +import VBodyScrollLock from 'src/directives/body_scroll_lock' + +import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' @@ -326,8 +332,8 @@ const checkOAuthToken = async ({ store }) => { } const afterStoreSetup = async ({ store, i18n }) => { - const width = windowWidth() - store.dispatch('setMobileLayout', width <= 800) + store.dispatch('setLayoutWidth', windowWidth()) + store.dispatch('setLayoutHeight', windowHeight()) FaviconService.initFaviconService() @@ -367,25 +373,32 @@ const afterStoreSetup = async ({ store, i18n }) => { getTOS({ store }) getStickers({ store }) - const router = new VueRouter({ - mode: 'history', + const router = createRouter({ + history: createWebHistory(), routes: routes(store), scrollBehavior: (to, _from, savedPosition) => { if (to.matched.some(m => m.meta.dontScroll)) { return false } - return savedPosition || { x: 0, y: 0 } + return savedPosition || { left: 0, top: 0 } } }) - /* eslint-disable no-new */ - return new Vue({ - router, - store, - i18n, - el: '#app', - render: h => h(App) - }) + const app = createApp(App) + + app.use(router) + app.use(store) + app.use(i18n) + + app.use(vClickOutside) + app.use(VBodyScrollLock) + + app.component('FAIcon', FontAwesomeIcon) + app.component('FALayers', FontAwesomeLayers) + + app.mount('#app') + + return app } export default afterStoreSetup diff --git a/src/boot/routes.js b/src/boot/routes.js index 1bc1f9f7da..726476a8a0 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -46,7 +46,7 @@ export default (store) => { { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'remote-user-profile-acct', - path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)', + path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', component: RemoteUserResolver, beforeEnter: validateAuthenticatedRoute }, @@ -62,14 +62,14 @@ export default (store) => { { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, - { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, + { name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, - { name: 'user-profile', path: '/(users/)?:name', component: UserProfile } + { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/about/about.vue b/src/components/about/about.vue index 518f6184da..5d5d647955 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -1,5 +1,5 @@ diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 137ef9c0ba..92ee3f3069 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,4 +1,4 @@ -import { set } from 'vue' +import { set } from 'lodash' import Select from '../select/select.vue' export default { @@ -6,11 +6,12 @@ export default { Select }, props: [ - 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' + 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], + emits: ['update:modelValue'], data () { return { - lValue: this.value, + lValue: this.modelValue, availableOptions: [ this.noInherit ? '' : 'inherit', 'custom', @@ -22,7 +23,7 @@ export default { } }, beforeUpdate () { - this.lValue = this.value + this.lValue = this.modelValue }, computed: { present () { @@ -37,7 +38,7 @@ export default { }, set (v) { set(this.lValue, 'family', v) - this.$emit('input', this.lValue) + this.$emit('update:modelValue', this.lValue) } }, isCustom () { diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index 296050841b..f100c3a9fa 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -15,13 +15,14 @@ class="opt exlcude-disabled" type="checkbox" :checked="present" - @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)" + @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" >