diff --git a/.babelrc b/.babelrc index 9fe4b349..bc2b0e31 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["es2015", "stage-2"], - "plugins": ["transform-runtime", "lodash"], + "presets": ["es2015", "stage-2", "env"], + "plugins": ["transform-runtime", "lodash", "transform-vue-jsx"], "comments": false } diff --git a/.eslintrc.js b/.eslintrc.js index 8e6549e5..800f9a4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { 'html' ], // add your custom rules here - 'rules': { + rules: { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await diff --git a/.gitignore b/.gitignore index faf39252..479d57c4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ test/unit/coverage test/e2e/reports selenium-debug.log .idea/ +config/local.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 694b77f9..6c83a123 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,32 +3,10 @@ # https://hub.docker.com/r/library/node/tags/ image: node:7 -before_script: - # Install ssh-agent if not already installed, it is required by Docker. - # (change apt-get to yum if you use a CentOS-based image) - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - - # Run ssh-agent (inside the build environment) - - eval $(ssh-agent -s) - - # For Docker builds disable host key checking. Be aware that by adding that - # you are suspectible to man-in-the-middle attacks. - # WARNING: Use this only with the Docker executor, if you use it with shell - # you will overwrite your user's SSH config. - - mkdir -p ~/.ssh - - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - -# This folder is cached between builds -# http://docs.gitlab.com/ce/ci/yaml/README.html#cache -#cache: -# paths: -# - node_modules/ - stages: - lint - build - test - - deploy lint: stage: lint @@ -50,14 +28,3 @@ build: artifacts: paths: - dist/ - -deploy: - stage: deploy - environment: dev - only: - - develop - script: - - yarn - - npm run build - - ssh-add <(echo "$SSH_PRIVATE_KEY") - - scp -r dist/* pleroma@tenshi.heldscal.la:~/pleroma diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md new file mode 100644 index 00000000..924c38da --- /dev/null +++ b/BREAKING_CHANGES.md @@ -0,0 +1,10 @@ +# v1.0 +## Removed features/radically changed behavior +### minimalScopesMode +As of !633, `scopeOptions` is no longer available and instead is changed for `minimalScopesMode` (default: `false`) + +Reasoning is that scopeOptions option originally existed mostly as a backwards-compatibility with GNU Social which only had `public` scope available and using scope selector would''t work. Since at some point we dropped GNU Social support, this option was mostly a nuisance (being default `false`'), however some people think scopes are an annoyance to a certain degree and want as less of that feature as possible. + +Solution - to only show minimal set among: *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from UI. + +*This setting is admin-default, user-configurable. Admin can choose different default for their instance but user can override it.* diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3673b8b7..d7c217ce 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,3 +8,4 @@ Contributors of this project. - hakui (hakui@freezepeach.xyz): CSS and styling - shpuld (shpuld@shitposter.club): CSS and styling - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. +- hj (hj@shigusegubu.club): Code diff --git a/README.md b/README.md index 5a3e2a4b..889f0837 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ # For Translators -To translate Pleroma, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. +To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. # FOR ADMINS -You don't need to build Pleroma yourself. Check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma and Qvitter at the same time. +You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. +For the GNU social backend, check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma-FE and Qvitter at the same time. ## Build Setup @@ -29,6 +30,21 @@ npm run build npm run unit ``` +# For Contributors: + +You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options: + +* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases. +* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode. + +FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE. + # Configuration -Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings. +Edit config.json for configuration. + +## Options + +### Login methods + +```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations. diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 7bba3a10..e07bb7a2 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -2,6 +2,7 @@ var path = require('path') var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') +var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -54,7 +55,7 @@ module.exports = { loader: 'vue' }, { - test: /\.js$/, + test: /\.jsx?$/, loader: 'babel', include: projectRoot, exclude: /node_modules\/(?!tributejs)/ @@ -91,5 +92,11 @@ module.exports = { browsers: ['last 2 versions'] }) ] - } + }, + plugins: [ + new ServiceWorkerWebpackPlugin({ + entry: path.join(__dirname, '..', 'src/sw.js'), + filename: 'sw-pleroma.js' + }) + ] } diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js index 7e1a104f..9f34619c 100644 --- a/build/webpack.dev.conf.js +++ b/build/webpack.dev.conf.js @@ -18,7 +18,9 @@ module.exports = merge(baseWebpackConfig, { devtool: '#eval-source-map', plugins: [ new webpack.DefinePlugin({ - 'process.env': config.dev.env + 'process.env': config.dev.env, + 'COMMIT_HASH': JSON.stringify('DEV'), + 'DEV_OVERRIDES': JSON.stringify(config.dev.settings) }), // https://github.com/glenjamin/webpack-hot-middleware#installation--usage new webpack.optimize.OccurenceOrderPlugin(), diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js index 6119f700..9699f221 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -7,8 +7,13 @@ var baseWebpackConfig = require('./webpack.base.conf') var ExtractTextPlugin = require('extract-text-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin') var env = process.env.NODE_ENV === 'testing' - ? require('../config/test.env') - : config.build.env + ? require('../config/test.env') + : config.build.env + +let commitHash = require('child_process') + .execSync('git rev-parse --short HEAD') + .toString(); +console.log(commitHash) var webpackConfig = merge(baseWebpackConfig, { module: { @@ -29,7 +34,9 @@ var webpackConfig = merge(baseWebpackConfig, { plugins: [ // http://vuejs.github.io/vue-loader/workflow/production.html new webpack.DefinePlugin({ - 'process.env': env + 'process.env': env, + 'COMMIT_HASH': JSON.stringify(commitHash), + 'DEV_OVERRIDES': JSON.stringify(undefined) }), new webpack.optimize.UglifyJsPlugin({ compress: { @@ -51,7 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, { minify: { removeComments: true, collapseWhitespace: true, - removeAttributeQuotes: true + removeAttributeQuotes: true, + ignoreCustomComments: [/server-generated-meta/] // more options: // https://github.com/kangax/html-minifier#options-quick-reference }, diff --git a/config/index.js b/config/index.js index c48d91b8..56fa5940 100644 --- a/config/index.js +++ b/config/index.js @@ -1,5 +1,15 @@ // see http://vuejs-templates.github.io/webpack for documentation. -var path = require('path') +const path = require('path') +let settings = {} +try { + settings = require('./local.json') + console.log('Using local dev server settings (/config/local.json):') + console.log(JSON.stringify(settings, null, 2)) +} catch (e) { + console.log('Local dev server settings not found (/config/local.json)') +} + +const target = settings.target || 'http://localhost:4000/' module.exports = { build: { @@ -19,16 +29,22 @@ module.exports = { dev: { env: require('./dev.env'), port: 8080, + settings, assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: { '/api': { - target: 'htts://localhost:4000/', + target, + changeOrigin: true, + cookieDomainRewrite: 'localhost' + }, + '/nodeinfo': { + target, changeOrigin: true, cookieDomainRewrite: 'localhost' }, '/socket': { - target: 'htts://localhost:4000/', + target, changeOrigin: true, cookieDomainRewrite: 'localhost', ws: true diff --git a/config/local.example.json b/config/local.example.json new file mode 100644 index 00000000..2a3bd00d --- /dev/null +++ b/config/local.example.json @@ -0,0 +1,4 @@ +{ + "target": "https://pleroma.soykaf.com/", + "staticConfigPreference": false +} diff --git a/index.html b/index.html index f0872ec9..d8defc2e 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ Pleroma + diff --git a/package.json b/package.json index 5718d24d..fcdea2c1 100644 --- a/package.json +++ b/package.json @@ -16,29 +16,41 @@ "dependencies": { "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-lodash": "^3.2.11", + "chromatism": "^3.0.0", + "cropperjs": "^1.4.3", "diff": "^3.0.1", "karma-mocha-reporter": "^2.2.1", "localforage": "^1.5.0", "node-sass": "^3.10.1", "object-path": "^0.11.3", "phoenix": "^1.3.0", + "popper.js": "^1.14.7", "sanitize-html": "^1.13.0", "sass-loader": "^4.0.2", + "v-click-outside": "^2.1.1", "vue": "^2.5.13", "vue-chat-scroll": "^1.2.1", "vue-i18n": "^7.3.2", + "vue-popperjs": "^2.0.3", "vue-router": "^3.0.1", "vue-template-compiler": "^2.3.4", "vue-timeago": "^3.1.2", + "vuelidate": "^0.7.4", "vuex": "^3.0.1", "whatwg-fetch": "^2.0.3" }, "devDependencies": { + "@babel/polyfill": "^7.0.0", + "@vue/test-utils": "^1.0.0-beta.26", "autoprefixer": "^6.4.0", "babel-core": "^6.0.0", "babel-eslint": "^7.0.0", + "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-loader": "^6.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-runtime": "^6.0.0", + "babel-plugin-transform-vue-jsx": "3", + "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.0.0", "babel-preset-stage-2": "^6.0.0", "babel-register": "^6.0.0", @@ -63,6 +75,7 @@ "html-webpack-plugin": "^2.8.1", "http-proxy-middleware": "^0.17.2", "inject-loader": "^2.0.1", + "iso-639-1": "^2.0.3", "isparta-loader": "^2.0.0", "json-loader": "^0.5.4", "karma": "^1.3.0", @@ -83,6 +96,7 @@ "raw-loader": "^0.5.1", "selenium-server": "2.53.1", "semver": "^5.3.0", + "serviceworker-webpack-plugin": "0.2.3", "shelljs": "^0.7.4", "sinon": "^1.17.3", "sinon-chai": "^2.8.0", diff --git a/src/App.js b/src/App.js index a052e058..46145b16 100644 --- a/src/App.js +++ b/src/App.js @@ -2,9 +2,15 @@ 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 UserFinder from './components/user_finder/user_finder.vue' -import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.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' import ChatPanel from './components/chat_panel/chat_panel.vue' +import MediaModal from './components/media_modal/media_modal.vue' +import SideDrawer from './components/side_drawer/side_drawer.vue' +import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue' +import MobileNav from './components/mobile_nav/mobile_nav.vue' +import { windowWidth } from './services/window_utils/window_utils' export default { name: 'app', @@ -13,34 +19,95 @@ export default { NavPanel, Notifications, UserFinder, - WhoToFollowPanel, InstanceSpecificPanel, - ChatPanel + FeaturesPanel, + WhoToFollowPanel, + ChatPanel, + MediaModal, + SideDrawer, + MobilePostStatusModal, + MobileNav }, data: () => ({ - mobileActivePanel: 'timeline' + mobileActivePanel: 'timeline', + finderHidden: true, + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || + window.CSS.supports('-webkit-mask-size', 'contain') || + window.CSS.supports('-moz-mask-size', 'contain') || + window.CSS.supports('-ms-mask-size', 'contain') || + window.CSS.supports('-o-mask-size', 'contain') + ) }), + created () { + // Load the locale from the storage + this.$i18n.locale = this.$store.state.config.interfaceLanguage + window.addEventListener('resize', this.updateMobileState) + }, + destroyed () { + window.removeEventListener('resize', this.updateMobileState) + }, computed: { currentUser () { return this.$store.state.users.currentUser }, background () { - return this.currentUser.background_image || this.$store.state.config.background + return this.currentUser.background_image || this.$store.state.instance.background }, - logoStyle () { return { 'background-image': `url(${this.$store.state.config.logo})` } }, - style () { return { 'background-image': `url(${this.background})` } }, - sitename () { return this.$store.state.config.name }, + enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, + logoStyle () { + return { + 'visibility': this.enableMask ? 'hidden' : 'visible' + } + }, + logoMaskStyle () { + return this.enableMask ? { + 'mask-image': `url(${this.$store.state.instance.logo})` + } : { + 'background-color': this.enableMask ? '' : 'transparent' + } + }, + logoBgStyle () { + return Object.assign({ + 'margin': `${this.$store.state.instance.logoMargin} 0`, + opacity: this.finderHidden ? 1 : 0 + }, this.enableMask ? {} : { + 'background-color': this.enableMask ? '' : 'transparent' + }) + }, + logo () { return this.$store.state.instance.logo }, + bgStyle () { + return { + 'background-image': `url(${this.background})` + } + }, + bgAppStyle () { + return { + '--body-background-image': `url(${this.background})` + } + }, + sitename () { return this.$store.state.instance.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, - showWhoToFollowPanel () { return this.$store.state.config.showWhoToFollowPanel }, - showInstanceSpecificPanel () { return this.$store.state.config.showInstanceSpecificPanel } + suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, + showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel }, + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + isMobileLayout () { return this.$store.state.interface.mobileLayout } }, methods: { - activatePanel (panelName) { - this.mobileActivePanel = panelName - }, scrollToTop () { window.scrollTo(0, 0) }, logout () { + this.$router.replace('/main/public') this.$store.dispatch('logout') + }, + onFinderToggled (hidden) { + this.finderHidden = hidden + }, + updateMobileState () { + const mobileLayout = windowWidth() <= 800 + const changed = mobileLayout !== this.isMobileLayout + if (changed) { + this.$store.dispatch('setMobileLayout', mobileLayout) + } } } } diff --git a/src/App.scss b/src/App.scss index f830a33b..b1c65ade 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,15 +1,21 @@ @import './_variables.scss'; #app { - background-size: cover; - background-attachment: fixed; - background-repeat: no-repeat; - background-position: 0 50px; min-height: 100vh; max-width: 100%; overflow: hidden; } +.app-bg-wrapper { + position: fixed; + z-index: -1; + height: 100%; + width: 100%; + background-size: cover; + background-repeat: no-repeat; + background-position: 0 50%; +} + i { user-select: none; } @@ -34,10 +40,11 @@ h4 { body { font-family: sans-serif; + font-family: var(--interfaceFont, sans-serif); font-size: 14px; margin: 0; - color: $fallback--fg; - color: var(--fg, $fallback--fg); + color: $fallback--text; + color: var(--text, $fallback--text); max-width: 100vw; overflow-x: hidden; } @@ -48,24 +55,39 @@ a { color: var(--link, $fallback--link); } -button{ +button { user-select: none; - color: $fallback--fg; - color: var(--fg, $fallback--fg); - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + color: $fallback--text; + color: var(--btnText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); border: none; border-radius: $fallback--btnRadius; border-radius: var(--btnRadius, $fallback--btnRadius); cursor: pointer; - border-top: 1px solid rgba(255, 255, 255, 0.2); - border-bottom: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0px 0px 2px black; + box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + box-shadow: var(--buttonShadow); font-size: 14px; font-family: sans-serif; + font-family: var(--interfaceFont, sans-serif); + + i[class*=icon-] { + color: $fallback--text; + color: var(--btnText, $fallback--text); + } + + &::-moz-focus-inner { + border: none; + } &:hover { box-shadow: 0px 0px 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: var(--buttonPressedShadow); } &:disabled { @@ -79,6 +101,14 @@ button{ background-color: $fallback--bg; background-color: var(--bg, $fallback--bg) } + + &.danger { + // TODO: add better color variable + color: $fallback--text; + color: var(--alertErrorPanelText, $fallback--text); + background-color: $fallback--alertError; + background-color: var(--alertError, $fallback--alertError); + } } label.select { @@ -90,21 +120,27 @@ input, textarea, .select { border: none; border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); - border-bottom: 1px solid rgba(255, 255, 255, 0.2); - border-top: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0px 0px 2px black inset; - background-color: $fallback--input; - background-color: var(--input, $fallback--input); - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + 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: var(--inputShadow); + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); + color: $fallback--lightText; + color: var(--inputText, $fallback--lightText); font-family: sans-serif; + font-family: var(--inputFont, sans-serif); font-size: 14px; - padding: 8px 7px; + padding: 8px .5em; box-sizing: border-box; display: inline-block; position: relative; - height: 29px; + height: 28px; line-height: 16px; + hyphens: none; + + &:disabled, &[disabled=disabled] { + cursor: not-allowed; + opacity: 0.5; + } .icon-down-open { position: absolute; @@ -112,9 +148,9 @@ input, textarea, .select { bottom: 0; right: 5px; height: 100%; - color: $fallback--fg; - color: var(--fg, $fallback--fg); - line-height: 29px; + color: $fallback--text; + color: var(--text, $fallback--text); + line-height: 28px; z-index: 0; pointer-events: none; } @@ -125,22 +161,40 @@ input, textarea, .select { appearance: none; background: transparent; border: none; + color: $fallback--text; + color: var(--inputText, --text, $fallback--text); margin: 0; - color: $fallback--fg; - color: var(--fg, $fallback--fg); - padding: 4px 2em 3px 3px; + padding: 0 2em 0 .2em; + font-family: sans-serif; + font-family: var(--inputFont, sans-serif); + font-size: 14px; width: 100%; z-index: 1; - height: 29px; + height: 28px; line-height: 16px; } + &[type=range] { + background: none; + border: none; + margin: 0; + box-shadow: none; + flex: 1; + } + &[type=radio], &[type=checkbox] { display: none; &:checked + label::before { - color: $fallback--fg; - color: var(--fg, $fallback--fg); + color: $fallback--text; + color: var(--text, $fallback--text); + } + &:disabled { + &, + & + label, + & + label::before { + opacity: .5; + } } + label::before { display: inline-block; @@ -148,14 +202,13 @@ input, textarea, .select { transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkBoxRadius; - border-radius: var(--checkBoxRadius, $fallback--checkBoxRadius); - border-bottom: 1px solid rgba(255, 255, 255, 0.2); - border-top: 1px solid rgba(0, 0, 0, 0.2); + border-radius: $fallback--checkboxRadius; + border-radius: var(--checkboxRadius, $fallback--checkboxRadius); box-shadow: 0px 0px 2px black inset; + box-shadow: var(--inputShadow); margin-right: .5em; - background-color: $fallback--input; - background-color: var(--input, $fallback--input); + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1em; @@ -168,6 +221,13 @@ input, textarea, .select { } } +option { + color: $fallback--text; + color: var(--text, $fallback--text); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); +} + i[class*=icon-] { color: $fallback--icon; color: var(--icon, $fallback--icon) @@ -181,51 +241,84 @@ i[class*=icon-] { padding: 0 10px 0 10px; } -.gaps { - margin: -1em 0 0 -1em; -} - .item { flex: 1; line-height: 50px; height: 50px; overflow: hidden; + display: flex; + flex-wrap: wrap; .nav-icon { - font-size: 1.1em; margin-left: 0.4em; } -} -.gaps > .item { - padding: 1em 0 0 1em; + &.right { + justify-content: flex-end; + } } .auto-size { flex: 1 } -nav { +.nav-bar { + padding: 0; width: 100%; align-items: center; position: fixed; height: 50px; + .logo { + display: flex; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + align-items: stretch; + justify-content: center; + flex: 0 0 auto; + z-index: -1; + transition: opacity; + transition-timing-function: ease-out; + transition-duration: 100ms; + + .mask { + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + background-color: $fallback--fg; + background-color: var(--topBarText, $fallback--fg); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + img { + height: 100%; + object-fit: contain; + display: block; + flex: 0; + } + } + .inner-nav { - padding-left: 20px; - padding-right: 20px; + margin: auto; + box-sizing: border-box; + padding-left: 10px; + padding-right: 10px; display: flex; align-items: center; flex-basis: 970px; - margin: auto; height: 50px; - background-repeat: no-repeat; - background-position: center; - background-size: auto 80%; - a i { + a, a i { color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--topBarLink, $fallback--link); } } } @@ -248,15 +341,33 @@ main-router { .panel { display: flex; + position: relative; + flex-direction: column; margin: 0.5em; background-color: $fallback--bg; background-color: var(--bg, $fallback--bg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); - box-shadow: 1px 1px 4px rgba(0,0,0,.6); + &::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 { @@ -267,15 +378,55 @@ main-router { } .panel-heading { + display: flex; border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; background-size: cover; - padding: 0.6em 1.0em; + padding: .6em .6em; text-align: left; - font-size: 1.3em; - line-height: 24px; - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + 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); + } + + .alert { + white-space: nowrap; + text-overflow: ellipsis; + overflow-x: hidden; + } + + button { + flex-shrink: 0; + } + + button, .alert { + // height: 100%; + line-height: 21px; + min-height: 0; + box-sizing: border-box; + margin: 0; + margin-left: .25em; + min-width: 1px; + align-self: stretch; + } + + a { + color: $fallback--link; + color: var(--panelLink, $fallback--link) + } } .panel-heading.stub { @@ -286,6 +437,17 @@ main-router { .panel-footer { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + + + .faint { + color: $fallback--faint; + color: var(--panelFaint, $fallback--faint); + } + + a { + color: $fallback--link; + color: var(--panelLink, $fallback--link) + } } .panel-body > p { @@ -304,11 +466,30 @@ main-router { nav { z-index: 1000; - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + 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); + + .back-button { + display: block; + max-width: 99px; + transition-property: opacity, max-width; + transition-duration: 300ms; + transition-timing-function: ease-out; + + i { + margin: 0 1em; + } + + &.hidden { + opacity: 0; + max-width: 5px; + } + } } .fade-enter-active, .fade-leave-active { @@ -319,7 +500,7 @@ nav { } .main { - flex-basis: 60%; + flex-basis: 50%; flex-grow: 1; flex-shrink: 1; } @@ -339,23 +520,17 @@ nav { display: none; } -.panel-switcher { - display: none; - width: 100%; - height: 46px; - button { - display: block; - flex: 1; - max-height: 32px; - margin: 0.5em; - padding: 0.5em; - } -} - -@media all and (min-width: 960px) { +@media all and (min-width: 800px) { body { overflow-y: scroll; } + + nav { + .back-button { + display: none; + } + } + .sidebar-bounds { overflow: hidden; max-height: 100vh; @@ -382,20 +557,46 @@ nav { flex-grow: 0; } } +.badge { + display: inline-block; + border-radius: 99px; + min-width: 22px; + max-width: 22px; + min-height: 22px; + max-height: 22px; + font-size: 15px; + line-height: 22px; + text-align: center; + vertical-align: middle; + white-space: nowrap; + padding: 0; + + &.badge-notification { + background-color: $fallback--cRed; + background-color: var(--badgeNotification, $fallback--cRed); + color: white; + color: var(--badgeNotificationText, white); + } +} .alert { margin: 0.35em; padding: 0.25em; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - color: $fallback--faint; - color: var(--faint, $fallback--faint); min-height: 28px; line-height: 28px; &.error { - background-color: $fallback--cAlertRed; - background-color: var(--cAlertRed, $fallback--cAlertRed); + background-color: $fallback--alertError; + background-color: var(--alertError, $fallback--alertError); + color: $fallback--text; + color: var(--alertErrorText, $fallback--text); + + .panel-heading & { + color: $fallback--text; + color: var(--alertErrorPanelText, $fallback--text); + } } } @@ -404,7 +605,105 @@ nav { color: var(--faint, $fallback--faint); } -@media all and (max-width: 959px) { +.faint-link { + color: $fallback--faint; + color: var(--faint, $fallback--faint); + + &:hover { + text-decoration: underline; + } +} + +@media all and (min-width: 800px) { + .logo { + opacity: 1 !important; + } +} + +.item.right { + text-align: right; +} + +.visibility-tray { + font-size: 1.2em; + padding: 3px; + cursor: pointer; + + .selected { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + + div { + padding-top: 5px; + } +} + +.visibility-notice { + padding: .5em; + border: 1px solid $fallback--faint; + border: 1px solid var(--faint, $fallback--faint); + border-radius: $fallback--inputRadius; + border-radius: var(--inputRadius, $fallback--inputRadius); +} + +@keyframes modal-background-fadein { + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: rgba(0, 0, 0, 0.5); + } +} + +.modal-view { + z-index: 1000; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: auto; + animation-duration: 0.2s; + background-color: rgba(0, 0, 0, 0.5); + animation-name: modal-background-fadein; +} + +.button-icon { + font-size: 1.2em; +} + +@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; } @@ -414,15 +713,84 @@ nav { } .container { - padding: 0 0 0 0; + padding: 0; } .panel { margin: 0.5em 0 0.5em 0; } + + .menu-button { + display: block; + margin-right: 0.8em; + } } -.item.right { - text-align: right; - padding-right: 20px; +.login-hint { + text-align: center; + + @media all and (min-width: 801px) { + display: none; + } + + a { + display: inline-block; + padding: 1em 0px; + width: 100%; + } } + +.btn.btn-default { + min-height: 28px; +} + +.autocomplete { + &-panel { + position: relative; + + &-body { + margin: 0 0.5em 0 0.5em; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + position: absolute; + z-index: 1; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); + // this doesn't match original but i don't care, making it uniform. + box-shadow: var(--popupShadow); + min-width: 75%; + background: $fallback--bg; + background: var(--bg, $fallback--bg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + &-item { + cursor: pointer; + padding: 0.2em 0.4em 0.2em 0.4em; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + display: flex; + + img { + width: 24px; + height: 24px; + object-fit: contain; + } + + span { + line-height: 24px; + margin: 0 0.1em 0 0.2em; + } + + small { + margin-left: .5em; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + + &.highlighted { + background-color: $fallback--fg; + background-color: var(--lightBg, $fallback--fg); + } + } +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 923d411b..3b8623ad 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,42 +1,51 @@ diff --git a/src/_variables.scss b/src/_variables.scss index b5222a6a..150e4fb5 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -3,24 +3,23 @@ $main-background: white; $darkened-background: whitesmoke; $fallback--bg: #121a24; -$fallback--btn: #182230; -$fallback--input: #182230; +$fallback--fg: #182230; $fallback--faint: rgba(185, 185, 186, .5); -$fallback--fg: #b9b9ba; +$fallback--text: #b9b9ba; $fallback--link: #d8a070; $fallback--icon: #666; $fallback--lightBg: rgb(21, 30, 42); -$fallback--lightFg: #b9b9ba; +$fallback--lightText: #b9b9ba; $fallback--border: #222; $fallback--cRed: #ff0000; $fallback--cBlue: #0095ff; $fallback--cGreen: #0fa00f; $fallback--cOrange: orange; -$fallback--cAlertRed: rgba(211,16,20,.5); +$fallback--alertError: rgba(211,16,20,.5); $fallback--panelRadius: 10px; -$fallback--checkBoxRadius: 2px; +$fallback--checkboxRadius: 2px; $fallback--btnRadius: 4px; $fallback--inputRadius: 4px; $fallback--tooltipRadius: 5px; diff --git a/src/assets/nsfw.png b/src/assets/nsfw.png index 42749033..d2513776 100644 Binary files a/src/assets/nsfw.png and b/src/assets/nsfw.png differ diff --git a/src/boot/after_store.js b/src/boot/after_store.js new file mode 100644 index 00000000..603de348 --- /dev/null +++ b/src/boot/after_store.js @@ -0,0 +1,293 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' +import routes from './routes' +import App from '../App.vue' +import { windowWidth } from '../services/window_utils/window_utils' + +const getStatusnetConfig = async ({ store }) => { + try { + const res = await window.fetch('/api/statusnet/config.json') + if (res.ok) { + const data = await res.json() + const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site + + store.dispatch('setInstanceOption', { name: 'name', value: name }) + store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) + store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) + store.dispatch('setInstanceOption', { name: 'server', value: server }) + store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' }) + + // TODO: default values for this stuff, added if to not make it break on + // my dev config out of the box. + if (uploadlimit) { + store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) }) + store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) }) + store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) }) + store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) }) + } + + if (vapidPublicKey) { + store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) + } + + return data.site.pleromafe + } else { + throw (res) + } + } catch (error) { + console.error('Could not load statusnet config, potentially fatal') + console.error(error) + } +} + +const getStaticConfig = async () => { + try { + const res = await window.fetch('/static/config.json') + if (res.ok) { + return res.json() + } else { + throw (res) + } + } catch (error) { + console.warn('Failed to load static/config.json, continuing without it.') + console.warn(error) + return {} + } +} + +const setSettings = async ({ apiConfig, staticConfig, store }) => { + const overrides = window.___pleromafe_dev_overrides || {} + const env = window.___pleromafe_mode.NODE_ENV + + // This takes static config and overrides properties that are present in apiConfig + let config = {} + if (overrides.staticConfigPreference && env === 'development') { + console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG') + config = Object.assign({}, apiConfig, staticConfig) + } else { + config = Object.assign({}, staticConfig, apiConfig) + } + + const copyInstanceOption = (name) => { + store.dispatch('setInstanceOption', { name, value: config[name] }) + } + + copyInstanceOption('nsfwCensorImage') + copyInstanceOption('background') + copyInstanceOption('hidePostStats') + copyInstanceOption('hideUserStats') + copyInstanceOption('hideFilteredStatuses') + copyInstanceOption('logo') + + store.dispatch('setInstanceOption', { + name: 'logoMask', + value: typeof config.logoMask === 'undefined' + ? true + : config.logoMask + }) + + store.dispatch('setInstanceOption', { + name: 'logoMargin', + value: typeof config.logoMargin === 'undefined' + ? 0 + : config.logoMargin + }) + + copyInstanceOption('redirectRootNoLogin') + copyInstanceOption('redirectRootLogin') + copyInstanceOption('showInstanceSpecificPanel') + copyInstanceOption('minimalScopesMode') + copyInstanceOption('formattingOptionsEnabled') + copyInstanceOption('hideMutedPosts') + copyInstanceOption('collapseMessageWithSubject') + copyInstanceOption('loginMethod') + copyInstanceOption('scopeCopy') + copyInstanceOption('subjectLineBehavior') + copyInstanceOption('postContentType') + copyInstanceOption('alwaysShowSubjectInput') + copyInstanceOption('noAttachmentLinks') + copyInstanceOption('showFeaturesPanel') + + if ((config.chatDisabled)) { + store.dispatch('disableChat') + } else { + store.dispatch('initializeSocket') + } + + return store.dispatch('setTheme', config['theme']) +} + +const getTOS = async ({ store }) => { + try { + const res = await window.fetch('/static/terms-of-service.html') + if (res.ok) { + const html = await res.text() + store.dispatch('setInstanceOption', { name: 'tos', value: html }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load TOS") + console.warn(e) + } +} + +const getInstancePanel = async ({ store }) => { + try { + const res = await window.fetch('/instance/panel.html') + if (res.ok) { + const html = await res.text() + store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load instance panel") + console.warn(e) + } +} + +const getStaticEmoji = async ({ store }) => { + try { + const res = await window.fetch('/static/emoji.json') + if (res.ok) { + const values = await res.json() + const emoji = Object.keys(values).map((key) => { + return { shortcode: key, image_url: false, 'utf': values[key] } + }) + store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load static emoji") + console.warn(e) + } +} + +// This is also used to indicate if we have a 'pleroma backend' or not. +// Somewhat weird, should probably be somewhere else. +const getCustomEmoji = async ({ store }) => { + try { + const res = await window.fetch('/api/pleroma/emoji.json') + if (res.ok) { + const result = await res.json() + const values = Array.isArray(result) ? Object.assign({}, ...result) : result + const emoji = Object.keys(values).map((key) => { + return { shortcode: key, image_url: values[key].image_url || values[key] } + }) + store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) + } else { + throw (res) + } + } catch (e) { + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) + console.warn("Can't load custom emojis, maybe not a Pleroma instance?") + console.warn(e) + } +} + +const getNodeInfo = async ({ store }) => { + try { + const res = await window.fetch('/nodeinfo/2.0.json') + if (res.ok) { + const data = await res.json() + const metadata = data.metadata + + const features = metadata.features + store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) + store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) + + store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) + store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) + + const suggestions = metadata.suggestions + store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) + store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) + + const software = data.software + store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + + const frontendVersion = window.___pleromafe_commit_hash + store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) + store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') }) + } else { + throw (res) + } + } catch (e) { + console.warn('Could not load nodeinfo') + console.warn(e) + } +} + +const setConfig = async ({ store }) => { + // apiConfig, staticConfig + const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()]) + const apiConfig = configInfos[0] + const staticConfig = configInfos[1] + + await setSettings({ store, apiConfig, staticConfig }) +} + +const checkOAuthToken = async ({ store }) => { + return new Promise(async (resolve, reject) => { + if (store.state.oauth.token) { + try { + await store.dispatch('loginUser', store.state.oauth.token) + } catch (e) { + console.log(e) + } + } + resolve() + }) +} + +const afterStoreSetup = async ({ store, i18n }) => { + if (store.state.config.customTheme) { + // This is a hack to deal with async loading of config.json and themes + // See: style_setter.js, setPreset() + window.themeLoaded = true + store.dispatch('setOption', { + name: 'customTheme', + value: store.state.config.customTheme + }) + } + + const width = windowWidth() + store.dispatch('setMobileLayout', width <= 800) + + // Now we can try getting the server settings and logging in + await Promise.all([ + checkOAuthToken({ store }), + setConfig({ store }), + getTOS({ store }), + getInstancePanel({ store }), + getStaticEmoji({ store }), + getCustomEmoji({ store }), + getNodeInfo({ store }) + ]) + + const router = new VueRouter({ + mode: 'history', + routes: routes(store), + scrollBehavior: (to, _from, savedPosition) => { + if (to.matched.some(m => m.meta.dontScroll)) { + return false + } + return savedPosition || { x: 0, y: 0 } + } + }) + + /* eslint-disable no-new */ + return new Vue({ + router, + store, + i18n, + el: '#app', + render: h => h(App) + }) +} + +export default afterStoreSetup diff --git a/src/boot/routes.js b/src/boot/routes.js new file mode 100644 index 00000000..7e54a98b --- /dev/null +++ b/src/boot/routes.js @@ -0,0 +1,53 @@ +import PublicTimeline from 'components/public_timeline/public_timeline.vue' +import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' +import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' +import TagTimeline from 'components/tag_timeline/tag_timeline.vue' +import ConversationPage from 'components/conversation-page/conversation-page.vue' +import Mentions from 'components/mentions/mentions.vue' +import DMs from 'components/dm_timeline/dm_timeline.vue' +import UserProfile from 'components/user_profile/user_profile.vue' +import Settings from 'components/settings/settings.vue' +import Registration from 'components/registration/registration.vue' +import UserSettings from 'components/user_settings/user_settings.vue' +import FollowRequests from 'components/follow_requests/follow_requests.vue' +import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' +import UserSearch from 'components/user_search/user_search.vue' +import Notifications from 'components/notifications/notifications.vue' +import LoginForm from 'components/login_form/login_form.vue' +import ChatPanel from 'components/chat_panel/chat_panel.vue' +import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' +import About from 'components/about/about.vue' + +export default (store) => { + return [ + { name: 'root', + path: '/', + redirect: _to => { + return (store.state.users.currentUser + ? store.state.instance.redirectRootLogin + : store.state.instance.redirectRootNoLogin) || '/main/all' + } + }, + { name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, + { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, + { name: 'friends', path: '/main/friends', component: FriendsTimeline }, + { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, + { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, + { name: 'external-user-profile', path: '/users/:id', component: UserProfile }, + { name: 'mentions', path: '/users/:username/mentions', component: Mentions }, + { name: 'dms', path: '/users/:username/dms', component: DMs }, + { name: 'settings', path: '/settings', component: Settings }, + { name: 'registration', path: '/registration', component: Registration }, + { name: 'registration-token', path: '/registration/:token', component: Registration }, + { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, + { name: 'user-settings', path: '/user-settings', component: UserSettings }, + { name: 'notifications', path: '/:username/notifications', component: Notifications }, + { name: 'login', path: '/login', component: LoginForm }, + { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, + { name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) }, + { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow }, + { name: 'about', path: '/about', component: About }, + { name: 'user-profile', path: '/(users/)?:name', component: UserProfile } + ] +} diff --git a/src/components/about/about.js b/src/components/about/about.js new file mode 100644 index 00000000..ae1cb182 --- /dev/null +++ b/src/components/about/about.js @@ -0,0 +1,16 @@ +import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue' +import FeaturesPanel from '../features_panel/features_panel.vue' +import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' + +const About = { + components: { + InstanceSpecificPanel, + FeaturesPanel, + TermsOfServicePanel + }, + computed: { + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel } + } +} + +export default About diff --git a/src/components/about/about.vue b/src/components/about/about.vue new file mode 100644 index 00000000..13dec87c --- /dev/null +++ b/src/components/about/about.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index d9bc4477..3b7f08dc 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -1,4 +1,5 @@ import StillImage from '../still-image/still-image.vue' +import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' @@ -7,21 +8,32 @@ const Attachment = { 'attachment', 'nsfw', 'statusId', - 'size' + 'size', + 'allowPlay', + 'setMedia' ], data () { return { - nsfwImage, + nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, hideNsfwLocal: this.$store.state.config.hideNsfw, - showHidden: false, + preloadImage: this.$store.state.config.preloadImage, loading: false, - img: document.createElement('img') + img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), + modalOpen: false, + showHidden: false } }, components: { - StillImage + StillImage, + VideoAttachment }, computed: { + usePlaceHolder () { + return this.size === 'hide' || this.type === 'unknown' + }, + referrerpolicy () { + return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' + }, type () { return fileTypeService.fileType(this.attachment.mimetype) }, @@ -35,7 +47,7 @@ const Attachment = { return this.size === 'small' }, fullwidth () { - return fileTypeService.fileType(this.attachment.mimetype) === 'html' + return this.type === 'html' || this.type === 'audio' } }, methods: { @@ -44,16 +56,37 @@ const Attachment = { window.open(target.href, '_blank') } }, - toggleHidden () { - if (this.img.onload) { - this.img.onload() - } else { - this.loading = true - this.img.src = this.attachment.url - this.img.onload = () => { - this.loading = false - this.showHidden = !this.showHidden + openModal (event) { + const modalTypes = this.$store.state.config.playVideosInModal + ? ['image', 'video'] + : ['image'] + if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || + this.usePlaceHolder + ) { + event.stopPropagation() + event.preventDefault() + this.setMedia() + this.$store.dispatch('setCurrent', this.attachment) + } + }, + toggleHidden (event) { + if (this.$store.state.config.useOneClickNsfw && !this.showHidden) { + this.openModal(event) + return + } + if (this.img && !this.preloadImage) { + if (this.img.onload) { + this.img.onload() + } else { + this.loading = true + this.img.src = this.attachment.url + this.img.onload = () => { + this.loading = false + this.showHidden = !this.showHidden + } } + } else { + this.showHidden = !this.showHidden } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index c48fb16b..c58bebd3 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,20 +1,44 @@