diff --git a/.babelrc b/.babelrc index 373d2c59..4ec10416 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], - "comments": false + "comments": true } diff --git a/.drone.yml b/.drone.yml index 4746f643..53140ab2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -28,8 +28,8 @@ steps: from_secret: SSH_KEY commands: - apt update && apt install -y wget dos2unix openssh-client rsync - - wget https://f.ruina.exposed/add-froth-key.sh && dos2unix ./add-froth-key.sh && chmod +x add-froth-key.sh && bash ./add-froth-key.sh - - wget https://f.ruina.exposed/pleroma-fe-build-froth.sh && dos2unix ./pleroma-fe-build-froth.sh && chmod +x pleroma-fe-build-froth.sh && bash ./pleroma-fe-build-froth.sh + - ./ci/add-key.sh + - ./ci/deploy.sh when: event: - push diff --git a/.eslintrc.js b/.eslintrc.js index 3c48baa8..361cff5f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ 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 @@ -21,6 +21,7 @@ module.exports = { 'generator-star-spacing': 0, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, - 'vue/require-prop-types': 0 + 'vue/require-prop-types': 0, + 'vue/multi-word-component-names': 0 } } diff --git a/.gitignore b/.gitignore index 479d57c4..a328a1b5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ test/e2e/reports selenium-debug.log .idea/ config/local.json +static/emoji.json + +.dccache \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab601173..305155d8 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:12 +image: node:16 stages: - lint diff --git a/.node-version b/.node-version index b26a34e4..431076a9 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -7.2.1 +16.16.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cc6994..b7eea727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,17 +16,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Attachment description is prefilled with backend-provided default when uploading - Proper visual feedback that next image is loading when browsing +- UI no longer lags when switching between mobile and desktop mode +- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything +- "Always show mobile button" is working now ### Changed +- Using Vue 3 now - (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) - User highlight background now also covers the `@` - Reverted back to textual `@`, svg version is opt-in. -- Settings window has been throughly rearranged to make make more sense and make navication settings easier. +- Settings window has been thoroughly rearranged to make more sense and make navigation settings easier. - Uploaded attachments are uniform with displayed attachments - Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) - Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. +- Slight width/spacing adjustments +- More sizing stuff is font-size dependent now +- Scrollbars are styled/colorized now +- Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in) ### Added +- 3 column mode: only enables when there's space for it (opt-out, customizable) - Options to show domains in mentions - Option to show user avatars in mention links (opt-in) - Option to disable the tooltip for mentions @@ -37,6 +46,8 @@ 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 +- Timelines/panels and conversations have sticky headers now +- Added frontend ui for account migration ## [2.4.2] - 2022-01-09 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f666a4ef..bfc41ac4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,3 +10,5 @@ Contributors of this project. - shpuld (shpuld@shitposter.club): CSS and styling - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. - hj (hj@shigusegubu.club): Code +- Sean King (seanking@freespeechextremist.com): Code +- Tusooa Zhu (tusooa@kazv.moe): Code diff --git a/add-froth-key.sh b/add-froth-key.sh new file mode 100644 index 00000000..64614f90 --- /dev/null +++ b/add-froth-key.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# only execute this script as part of the pipeline. +[ -z "$CI" ] && echo "missing ci environment variable" && exit 2 + +# only execute the script when github token exists. +[ -z "$SSH_KEY" ] && echo "missing ssh key" && exit 3 + +# write the ssh key. +mkdir /root/.ssh +echo -n "${SSH_KEY}" > /root/.ssh/id_ed25519 +chmod 600 /root/.ssh/id_ed25519 + +# add froth.zone to our known hosts. +touch /root/.ssh/known_hosts +chmod 600 /root/.ssh/known_hosts +ssh-keyscan -H froth.zone > /etc/ssh/ssh_known_hosts 2> /dev/null diff --git a/build/build.js b/build/build.js index b3c9aad4..35969eb6 100644 --- a/build/build.js +++ b/build/build.js @@ -18,6 +18,9 @@ console.log( var spinner = ora('building for production...') spinner.start() +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) rm('-rf', assetsPath) mkdir('-p', assetsPath) diff --git a/build/dev-server.js b/build/dev-server.js index 48574214..536265f9 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing' ? require('./webpack.prod.conf') : require('./webpack.dev.conf') +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + // default port where dev server listens for incoming traffic var port = process.env.PORT || config.dev.port // Define HTTP proxies to your custom API backend @@ -28,18 +31,20 @@ 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() - }) -}) +// FIXME: The statement below gives error about hooks being required in webpack 5. +// 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) { @@ -47,7 +52,7 @@ Object.keys(proxyTable).forEach(function (context) { if (typeof options === 'string') { options = { target: options } } - app.use(proxyMiddleware(context, options)) + app.use(proxyMiddleware.createProxyMiddleware(context, options)) }) // handle fallback for HTML5 history API diff --git a/build/update-emoji.js b/build/update-emoji.js new file mode 100644 index 00000000..9f4b4e67 --- /dev/null +++ b/build/update-emoji.js @@ -0,0 +1,27 @@ + +module.exports = { + updateEmoji () { + const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group') + const fs = require('fs') + + Object.keys(emojis) + .map(k => { + emojis[k].map(e => { + delete e.unicode_version + delete e.emoji_version + delete e.skin_tone_support_unicode_version + }) + }) + + const res = {} + Object.keys(emojis) + .map(k => { + const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() + res[groupId] = emojis[k] + }) + + console.info('Updating emojis...') + fs.writeFileSync('static/emoji.json', JSON.stringify(res)) + console.info('Done.') + } +} diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 615464a5..bf946922 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -2,8 +2,11 @@ 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"); +var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin') +var CopyPlugin = require('copy-webpack-plugin'); +var { VueLoaderPlugin } = require('vue-loader') +var ESLintPlugin = require('eslint-webpack-plugin'); + var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -21,7 +24,8 @@ module.exports = { output: { path: config.build.assetsRoot, publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, - filename: '[name].js' + filename: '[name].js', + chunkFilename: '[name].js' }, optimization: { splitChunks: { @@ -29,7 +33,7 @@ module.exports = { } }, resolve: { - extensions: ['.js', '.jsx', '.vue'], + extensions: ['.mjs', '.js', '.jsx', '.vue'], modules: [ path.join(__dirname, '../node_modules') ], @@ -39,25 +43,15 @@ module.exports = { 'assets': path.resolve(__dirname, '../src/assets'), 'components': path.resolve(__dirname, '../src/components'), 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' + }, + fallback: { + 'querystring': require.resolve('querystring-es3'), + 'url': require.resolve('url/') } }, module: { noParse: /node_modules\/localforage\/dist\/localforage.js/, rules: [ - { - enforce: 'pre', - test: /\.(js|vue)$/, - include: projectRoot, - exclude: /node_modules/, - use: { - loader: 'eslint-loader', - options: { - formatter: require('eslint-friendly-formatter'), - sourceMap: config.build.productionSourceMap, - extract: true - } - } - }, { enforce: 'post', test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files @@ -89,24 +83,23 @@ module.exports = { }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, - use: { - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('img/[name].[hash:7].[ext]') - } + type: 'asset', + generator: { + filename: utils.assetsPath('img/[name].[hash:7][ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, - use: { - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('fonts/[name].[hash:7].[ext]') - } + type: 'asset', + generator: { + filename: utils.assetsPath('fonts/[name].[hash:7][ext]') } }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto' + } ] }, plugins: [ @@ -114,6 +107,22 @@ module.exports = { entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' }), - new VueLoaderPlugin() + new ESLintPlugin({ + extensions: ['js', 'vue'], + formatter: require('eslint-formatter-friendly') + }), + new VueLoaderPlugin(), + // This copies Ruffle's WASM to a directory so that JS side can access it + new CopyPlugin({ + patterns: [ + { + from: "node_modules/@ruffle-rs/ruffle/**/*", + to: "static/ruffle/[name][ext]" + }, + ], + options: { + concurrency: 100, + }, + }) ] } diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js index 4605b93d..97799f82 100644 --- a/build/webpack.dev.conf.js +++ b/build/webpack.dev.conf.js @@ -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..7de93721 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -5,6 +5,7 @@ var webpack = require('webpack') var merge = require('webpack-merge') var baseWebpackConfig = require('./webpack.base.conf') var MiniCssExtractPlugin = require('mini-css-extract-plugin') +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") var HtmlWebpackPlugin = require('html-webpack-plugin') var env = process.env.NODE_ENV === 'testing' ? require('../config/test.env') @@ -19,12 +20,16 @@ var webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true }) }, - devtool: config.build.productionSourceMap ? '#source-map' : false, + devtool: config.build.productionSourceMap ? 'source-map' : false, optimization: { minimize: true, splitChunks: { chunks: 'all' - } + }, + minimizer: [ + `...`, + new CssMinimizerPlugin() + ] }, output: { path: config.build.assetsRoot, @@ -60,9 +65,7 @@ var webpackConfig = merge(baseWebpackConfig, { ignoreCustomComments: [/server-generated-meta/] // more options: // https://github.com/kangax/html-minifier#options-quick-reference - }, - // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency' + } }), // split vendor js into its own file // extract webpack runtime and module manifest to its own file in order to diff --git a/ci/add-key.sh b/ci/add-key.sh new file mode 100755 index 00000000..64614f90 --- /dev/null +++ b/ci/add-key.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# only execute this script as part of the pipeline. +[ -z "$CI" ] && echo "missing ci environment variable" && exit 2 + +# only execute the script when github token exists. +[ -z "$SSH_KEY" ] && echo "missing ssh key" && exit 3 + +# write the ssh key. +mkdir /root/.ssh +echo -n "${SSH_KEY}" > /root/.ssh/id_ed25519 +chmod 600 /root/.ssh/id_ed25519 + +# add froth.zone to our known hosts. +touch /root/.ssh/known_hosts +chmod 600 /root/.ssh/known_hosts +ssh-keyscan -H froth.zone > /etc/ssh/ssh_known_hosts 2> /dev/null diff --git a/ci/deploy.sh b/ci/deploy.sh new file mode 100755 index 00000000..9625d473 --- /dev/null +++ b/ci/deploy.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +TARGET="pleroma@froth.zone:/opt/pleroma" + +#rsync -ra public/ "${TARGET}/instance/static" +#cp dist/index.html "${TARGET}/instance/static/index.html" +rsync --update -Pr dist/ "${TARGET}/instance/static/" +rsync --update -ra dist/static/ "${TARGET}/instance/static/static" +#rsync --delete -ra images/ "${TARGET}/instance/static/images" +#rsync --delete -ra sounds/ "${TARGET}/instance/static/sounds" +rsync -ra instance/ "${TARGET}/instance/static/instance" +#rsync --delete -ra pages/ "${TARGET}/instance/static/pages" diff --git a/package.json b/package.json index 58aeb363..260df573 100644 --- a/package.json +++ b/package.json @@ -16,111 +16,112 @@ "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "7.17.8", + "@babel/runtime": "7.18.9", "@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": "3.0.0-5", + "@fortawesome/fontawesome-svg-core": "6.2.0", + "@fortawesome/free-regular-svg-icons": "6.2.0", + "@fortawesome/free-solid-svg-icons": "6.2.0", + "@fortawesome/vue-fontawesome": "3.0.1", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", - "@vuelidate/core": "2.0.0-alpha.41", - "@vuelidate/validators": "2.0.0-alpha.27", - "body-scroll-lock": "2.7.1", + "@kazvmoe-infra/unicode-emoji-json": "^0.4.0", + "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", + "@vuelidate/core": "2.0.0-alpha.44", + "@vuelidate/validators": "2.0.0-alpha.31", + "body-scroll-lock": "3.1.5", "chromatism": "3.0.0", "click-outside-vue3": "4.0.1", "cropperjs": "1.5.12", "diff": "3.5.0", "escape-html": "1.0.3", - "js-cookie": "^3.0.1", + "js-cookie": "3.0.1", "localforage": "1.10.0", - "parse-link-header": "1.0.1", + "lozad": "^1.16.0", + "parse-link-header": "2.0.0", "phoenix": "1.6.2", "punycode.js": "2.1.0", - "qrcode": "1", - "ruffle-mirror": "2021.12.31", - "vue": "^3.2.31", - "vue-i18n": "^9.2.0-beta.34", - "vue-router": "4.0.14", - "vue-template-compiler": "2.6.11", + "qrcode": "1.5.0", + "querystring-es3": "0.2.1", + "url": "0.11.0", + "utf8": "3.0.0", + "vue": "3.2.38", + "vue-i18n": "9.2.2", + "vue-router": "4.1.5", + "vue-template-compiler": "2.7.10", "vuex": "4.0.2" }, "devDependencies": { - "@babel/core": "7.17.8", - "@babel/plugin-transform-runtime": "7.17.0", - "@babel/preset-env": "7.16.11", - "@babel/register": "7.17.7", - "@intlify/vue-i18n-loader": "^5.0.0", + "@babel/core": "7.18.13", + "@babel/eslint-parser": "7.18.9", + "@babel/plugin-transform-runtime": "7.18.10", + "@babel/preset-env": "7.18.10", + "@babel/register": "7.18.9", + "@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-helper-vue-jsx-merge-props": "1.4.0", "@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.4", + "@vue/compiler-sfc": "3.2.38", + "@vue/test-utils": "2.0.2", + "autoprefixer": "10.4.8", + "babel-loader": "8.2.5", "babel-plugin-lodash": "3.3.4", - "chai": "3.5.0", + "chai": "4.3.6", "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", + "chromedriver": "104.0.0", + "connect-history-api-fallback": "2.0.0", + "copy-webpack-plugin": "11.0.0", + "cross-spawn": "7.0.3", + "css-loader": "6.7.1", + "css-minimizer-webpack-plugin": "4.0.0", "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", + "eslint": "8.23.0", + "eslint-config-standard": "17.0.0", + "eslint-formatter-friendly": "7.0.0", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-n": "15.2.5", + "eslint-plugin-promise": "6.0.1", + "eslint-plugin-vue": "9.4.0", + "eslint-webpack-plugin": "3.2.0", "eventsource-polyfill": "0.9.6", - "express": "4.17.3", - "file-loader": "3.0.1", + "express": "4.18.1", "function-bind": "1.1.1", - "html-webpack-plugin": "3.2.0", - "http-proxy-middleware": "0.21.0", - "inject-loader": "2.0.1", - "iso-639-1": "2.1.13", - "isparta-loader": "2.0.0", + "html-webpack-plugin": "5.5.0", + "http-proxy-middleware": "2.0.6", + "iso-639-1": "2.1.15", "json-loader": "0.5.7", - "karma": "6.3.17", - "karma-coverage": "1.1.2", - "karma-firefox-launcher": "1.3.0", + "karma": "6.4.0", + "karma-coverage": "2.2.0", + "karma-firefox-launcher": "2.1.2", "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-webpack": "4.0.2", + "karma-spec-reporter": "0.0.34", + "karma-webpack": "5.0.0", "lodash": "4.17.21", "lolex": "1.6.0", - "mini-css-extract-plugin": "0.12.0", - "mocha": "3.5.3", - "nightwatch": "0.9.21", - "opn": "4.0.2", + "mini-css-extract-plugin": "2.6.1", + "mocha": "10.0.0", + "nightwatch": "2.3.3", + "opn": "5.5.0", "ora": "0.4.1", - "postcss-loader": "3.0.0", - "raw-loader": "0.5.1", - "sass": "1.20.1", - "sass-loader": "7.2.0", + "postcss": "8.4.16", + "postcss-loader": "7.0.1", + "sass": "1.54.8", + "sass-loader": "13.0.2", "selenium-server": "2.53.1", - "semver": "5.7.1", - "serviceworker-webpack-plugin": "1.0.1", + "semver": "7.3.7", + "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", - "sinon": "2.4.1", - "sinon-chai": "2.14.0", - "stylelint": "13.6.1", + "sinon": "14.0.0", + "sinon-chai": "3.7.0", + "stylelint": "13.13.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", + "vue-loader": "17.0.0", + "vue-style-loader": "4.1.3", + "webpack": "5.74.0", "webpack-dev-middleware": "3.7.3", - "webpack-hot-middleware": "2.24.3", + "webpack-hot-middleware": "2.25.2", "webpack-merge": "0.20.0" }, "engines": { diff --git a/src/App.js b/src/App.js index f01f8788..b7eb2f72 100644 --- a/src/App.js +++ b/src/App.js @@ -4,14 +4,15 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import ShoutPanel from './components/shout_panel/shout_panel.vue' -import SettingsModal from './components/settings_modal/settings_modal.vue' import MediaModal from './components/media_modal/media_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' import DesktopNav from './components/desktop_nav/desktop_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' +import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' +import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' @@ -32,9 +33,12 @@ export default { MobilePostStatusButton, MobileNav, DesktopNav, - SettingsModal, + SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')), + UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')), UserReportingModal, PostStatusModal, + EditStatusModal, + StatusHistoryModal, GlobalNoticeList }, data: () => ({ @@ -60,6 +64,13 @@ export default { '-' + this.layoutType ] }, + navClasses () { + const { navbarColumnStretch } = this.$store.getters.mergedConfig + return [ + '-' + this.layoutType, + ...(navbarColumnStretch ? ['-column-stretch'] : []) + ] + }, currentUser () { return this.$store.state.users.currentUser }, userBackground () { return this.currentUser.background_image }, instanceBackground () { @@ -85,11 +96,16 @@ export default { isChats () { return this.$route.name === 'chat' || this.$route.name === 'chats' }, + isListEdit () { + return this.$route.name === 'lists-edit' + }, newPostButtonShown () { if (this.isChats) return false + if (this.isListEdit) return false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + editingAvailable () { return this.$store.state.instance.editingAvailable }, shoutboxPosition () { return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false }, diff --git a/src/App.scss b/src/App.scss index 5cd0b96e..75b2667c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -4,6 +4,13 @@ :root { --navbar-height: 3.5rem; --post-line-height: 1.4; + // Z-Index stuff + --ZI_media_modal: 9000; + --ZI_modals_popovers: 8500; + --ZI_modals: 8000; + --ZI_navbar_popovers: 7500; + --ZI_navbar: 7000; + --ZI_popovers: 6000; } html { @@ -110,14 +117,30 @@ h4 { margin: 0; } +.iconLetter { + display: inline-block; + text-align: center; + font-weight: 1000; +} + i[class*=icon-], -.svg-inline--fa { +.svg-inline--fa, +.iconLetter { color: $fallback--icon; color: var(--icon, $fallback--icon); } +.button-unstyled:hover, +a:hover { + > i[class*=icon-], + > .svg-inline--fa, + > .iconLetter { + color: var(--text); + } +} + nav { - z-index: 1000; + z-index: var(--ZI_navbar); color: var(--topBarText); background-color: $fallback--fg; background-color: var(--topBar, $fallback--fg); @@ -134,6 +157,11 @@ nav { grid-area: sidebar; } +#modal { + position: absolute; + z-index: var(--ZI_modals); +} + .column.-scrollable { top: var(--navbar-height); position: sticky; @@ -175,13 +203,18 @@ nav { .app-layout { --miniColumn: 25rem; - --maxiColumn: minmax(var(--miniColumn), 45rem); + --maxiColumn: 45rem; --columnGap: 1em; --status-margin: 0.75em; + --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); + --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); + --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); position: relative; display: grid; - grid-template-columns: var(--miniColumn) var(--maxiColumn); + grid-template-columns: + var(--effectiveSidebarColumnWidth) + var(--effectiveContentColumnWidth); grid-template-areas: "sidebar content"; grid-template-rows: 1fr; box-sizing: border-box; @@ -275,15 +308,24 @@ nav { } &.-reverse:not(.-wide):not(.-mobile) { - grid-template-columns: var(--maxiColumn) var(--miniColumn); + grid-template-columns: + var(--effectiveContentColumnWidth) + var(--effectiveSidebarColumnWidth); grid-template-areas: "content sidebar"; } &.-wide { - grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn); + grid-template-columns: + var(--effectiveSidebarColumnWidth) + var(--effectiveContentColumnWidth) + var(--effectiveNotifsColumnWidth); grid-template-areas: "sidebar content notifs"; &.-reverse { + grid-template-columns: + var(--effectiveNotifsColumnWidth) + var(--effectiveContentColumnWidth) + var(--effectiveSidebarColumnWidth); grid-template-areas: "notifs content sidebar"; } } @@ -310,7 +352,6 @@ nav { border-top-right-radius: 0; } - .underlay, #sidebar, #notifs-column { display: none; @@ -740,17 +781,23 @@ option { } .fa-scale-110 { - &.svg-inline--fa { + &.svg-inline--fa, + &.iconLetter { font-size: 1.1em; } } .fa-old-padding { - &.svg-inline--fa { + &.iconLetter, + &.svg-inline--fa, &-layer { padding: 0 0.3em; } } +.veryfaint { + opacity: 0.25; +} + .login-hint { text-align: center; @@ -829,7 +876,7 @@ option { // Vue transitions .fade-enter-active, .fade-leave-active { - transition: opacity 0.2s; + transition: opacity 0.3s; } .fade-enter-from, diff --git a/src/App.vue b/src/App.vue index d1d4217b..e0d709f7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,10 @@ class="app-bg-wrapper" /> - +
- + + + + - + diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 99762562..c23407f9 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,6 +1,7 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV @@ -19,7 +20,8 @@ const AccountActions = { }, components: { ProgressButton, - Popover + Popover, + UserListMenu }, methods: { showRepeats () { @@ -34,6 +36,9 @@ const AccountActions = { unblockUser () { this.$store.dispatch('unblockUser', this.user.id) }, + removeUserFromFollowers () { + this.$store.dispatch('removeUserFromFollowers', this.user.id) + }, reportUser () { this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) }, diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index c35d01af..218aa6b3 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -6,7 +6,7 @@ :bound-to="{ x: 'container' }" remove-padding > - - + diff --git a/src/components/lists_card/lists_card.js b/src/components/lists_card/lists_card.js new file mode 100644 index 00000000..b503caec --- /dev/null +++ b/src/components/lists_card/lists_card.js @@ -0,0 +1,16 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisH +) + +const ListsCard = { + props: [ + 'list' + ] +} + +export default ListsCard diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue new file mode 100644 index 00000000..13866d8c --- /dev/null +++ b/src/components/lists_card/lists_card.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js new file mode 100644 index 00000000..c22d1323 --- /dev/null +++ b/src/components/lists_edit/lists_edit.js @@ -0,0 +1,145 @@ +import { mapState, mapGetters } from 'vuex' +import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import ListsUserSearch from '../lists_user_search/lists_user_search.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' +import UserAvatar from '../user_avatar/user_avatar.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSearch, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSearch, + faChevronLeft +) + +const ListsNew = { + components: { + BasicUserCard, + UserAvatar, + ListsUserSearch, + TabSwitcher, + PanelLoading + }, + data () { + return { + title: '', + titleDraft: '', + membersUserIds: [], + removedUserIds: new Set([]), // users we added for members, to undo + searchUserIds: [], + addedUserIds: new Set([]), // users we added from search, to undo + searchLoading: false, + reallyDelete: false + } + }, + created () { + if (!this.id) return + this.$store.dispatch('fetchList', { listId: this.id }) + .then(() => { + this.title = this.findListTitle(this.id) + this.titleDraft = this.title + }) + this.$store.dispatch('fetchListAccounts', { listId: this.id }) + .then(() => { + this.membersUserIds = this.findListAccounts(this.id) + this.membersUserIds.forEach(userId => { + this.$store.dispatch('fetchUserIfMissing', userId) + }) + }) + }, + computed: { + id () { + return this.$route.params.id + }, + membersUsers () { + return [...this.membersUserIds, ...this.addedUserIds] + .map(userId => this.findUser(userId)).filter(user => user) + }, + searchUsers () { + return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user) + }, + ...mapState({ + currentUser: state => state.users.currentUser + }), + ...mapGetters(['findUser', 'findListTitle', 'findListAccounts']) + }, + methods: { + onInput () { + this.search(this.query) + }, + toggleRemoveMember (user) { + if (this.removedUserIds.has(user.id)) { + this.id && this.addUser(user) + this.removedUserIds.delete(user.id) + } else { + this.id && this.removeUser(user.id) + this.removedUserIds.add(user.id) + } + }, + toggleAddFromSearch (user) { + if (this.addedUserIds.has(user.id)) { + this.id && this.removeUser(user.id) + this.addedUserIds.delete(user.id) + } else { + this.id && this.addUser(user) + this.addedUserIds.add(user.id) + } + }, + isRemoved (user) { + return this.removedUserIds.has(user.id) + }, + isAdded (user) { + return this.addedUserIds.has(user.id) + }, + addUser (user) { + this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id }) + }, + removeUser (userId) { + this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) + }, + onSearchLoading (results) { + this.searchLoading = true + }, + onSearchLoadingDone (results) { + this.searchLoading = false + }, + onSearchResults (results) { + this.searchLoading = false + this.searchUserIds = results + }, + updateListTitle () { + this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + .then(() => { + this.title = this.findListTitle(this.id) + }) + }, + createList () { + this.$store.dispatch('createList', { title: this.titleDraft }) + .then((list) => { + return this + .$store + .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] }) + .then(() => list.id) + }) + .then((listId) => { + this.$router.push({ name: 'lists-timeline', params: { id: listId } }) + }) + .catch((e) => { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'lists.error', + messageArgs: [e.message], + level: 'error' + }) + }) + }, + deleteList () { + this.$store.dispatch('deleteList', { listId: this.id }) + this.$router.push({ name: 'lists' }) + } + } +} + +export default ListsNew diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue new file mode 100644 index 00000000..6521aba6 --- /dev/null +++ b/src/components/lists_edit/lists_edit.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js new file mode 100644 index 00000000..97b32210 --- /dev/null +++ b/src/components/lists_menu/lists_menu_content.js @@ -0,0 +1,22 @@ +import { mapState } from 'vuex' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { getListEntries } from 'src/components/navigation/filter.js' + +export const ListsMenuContent = { + props: [ + 'showPin' + ], + components: { + NavigationEntry + }, + computed: { + ...mapState({ + lists: getListEntries, + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default ListsMenuContent diff --git a/src/components/lists_menu/lists_menu_content.vue b/src/components/lists_menu/lists_menu_content.vue new file mode 100644 index 00000000..f93e80c9 --- /dev/null +++ b/src/components/lists_menu/lists_menu_content.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js new file mode 100644 index 00000000..c3f408bd --- /dev/null +++ b/src/components/lists_timeline/lists_timeline.js @@ -0,0 +1,36 @@ +import Timeline from '../timeline/timeline.vue' +const ListsTimeline = { + data () { + return { + listId: null + } + }, + components: { + Timeline + }, + computed: { + timeline () { return this.$store.state.statuses.timelines.list } + }, + watch: { + $route: function (route) { + if (route.name === 'lists-timeline' && route.params.id !== this.listId) { + this.listId = route.params.id + this.$store.dispatch('stopFetchingTimeline', 'list') + this.$store.commit('clearTimeline', { timeline: 'list' }) + this.$store.dispatch('fetchList', { listId: this.listId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) + } + } + }, + created () { + this.listId = this.$route.params.id + this.$store.dispatch('fetchList', { listId: this.listId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'list') + this.$store.commit('clearTimeline', { timeline: 'list' }) + } +} + +export default ListsTimeline diff --git a/src/components/lists_timeline/lists_timeline.vue b/src/components/lists_timeline/lists_timeline.vue new file mode 100644 index 00000000..18156b81 --- /dev/null +++ b/src/components/lists_timeline/lists_timeline.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js new file mode 100644 index 00000000..c92ec0ee --- /dev/null +++ b/src/components/lists_user_search/lists_user_search.js @@ -0,0 +1,51 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSearch, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' +import { debounce } from 'lodash' +import Checkbox from '../checkbox/checkbox.vue' + +library.add( + faSearch, + faChevronLeft +) + +const ListsUserSearch = { + components: { + Checkbox + }, + emits: ['loading', 'loadingDone', 'results'], + data () { + return { + loading: false, + query: '', + followingOnly: true + } + }, + methods: { + onInput: debounce(function () { + this.search(this.query) + }, 2000), + search (query) { + if (!query) { + this.loading = false + return + } + + this.loading = true + this.$emit('loading') + this.userIds = [] + this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly }) + .then(data => { + this.$emit('results', data.accounts.map(a => a.id)) + }) + .finally(() => { + this.loading = false + this.$emit('loadingDone') + }) + } + } +} + +export default ListsUserSearch diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue new file mode 100644 index 00000000..8633170c --- /dev/null +++ b/src/components/lists_user_search/lists_user_search.vue @@ -0,0 +1,47 @@ + + + + diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 638bd812..b795640e 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -83,7 +83,7 @@ const LoginForm = { }, clearError () { this.error = false }, focusOnPasswordInput () { - let passwordInput = this.$refs.passwordInput + const passwordInput = this.$refs.passwordInput passwordInput.focus() passwordInput.setSelectionRange(0, passwordInput.value.length) } diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index 21482977..7a430c51 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -90,7 +90,7 @@
- + diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js new file mode 100644 index 00000000..31b55486 --- /dev/null +++ b/src/components/navigation/filter.js @@ -0,0 +1,18 @@ +export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => { + return list.filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!isFederating && set.has('federating')) return false + if (isPrivate && set.has('!private')) return false + if (!currentUser && !(anon || anonRoute)) return false + if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false + if (!hasChats && set.has('chats')) return false + return true + }) +} + +export const getListEntries = state => state.lists.allLists.map(list => ({ + name: 'list-' + list.id, + routeObject: { name: 'lists-timeline', params: { id: list.id } }, + labelRaw: list.title, + iconLetter: list.title[0] +})) diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js new file mode 100644 index 00000000..f66dd981 --- /dev/null +++ b/src/components/navigation/navigation.js @@ -0,0 +1,75 @@ +export const USERNAME_ROUTES = new Set([ + 'bookmarks', + 'dms', + 'interactions', + 'notifications', + 'chat', + 'chats', + 'user-profile' +]) + +export const TIMELINES = { + home: { + route: 'friends', + icon: 'home', + label: 'nav.home_timeline', + criteria: ['!private'] + }, + public: { + route: 'public-timeline', + anon: true, + icon: 'users', + label: 'nav.public_tl', + criteria: ['!private'] + }, + twkn: { + route: 'public-external-timeline', + anon: true, + icon: 'globe', + label: 'nav.twkn', + criteria: ['!private', 'federating'] + }, + bookmarks: { + route: 'bookmarks', + icon: 'bookmark', + label: 'nav.bookmarks' + }, + favorites: { + routeObject: { name: 'user-profile', query: { tab: 'favorites' } }, + icon: 'star', + label: 'user_card.favorites' + }, + dms: { + route: 'dms', + icon: 'envelope', + label: 'nav.dms' + } +} + +export const ROOT_ITEMS = { + interactions: { + route: 'interactions', + icon: 'bell', + label: 'nav.interactions' + }, + chats: { + route: 'chats', + icon: 'comments', + label: 'nav.chats', + badgeGetter: 'unreadChatCount', + criteria: ['chats'] + }, + friendRequests: { + route: 'friend-requests', + icon: 'user-plus', + label: 'nav.friend_requests', + criteria: ['lockedUser'], + badgeGetter: 'followRequestCount' + }, + about: { + route: 'about', + anon: true, + icon: 'info-circle', + label: 'nav.about' + } +} diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js new file mode 100644 index 00000000..81cc936a --- /dev/null +++ b/src/components/navigation/navigation_entry.js @@ -0,0 +1,51 @@ +import { mapState } from 'vuex' +import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faThumbtack } from '@fortawesome/free-solid-svg-icons' + +library.add(faThumbtack) + +const NavigationEntry = { + props: ['item', 'showPin'], + components: { + OptionalRouterLink + }, + methods: { + isPinned (value) { + return this.pinnedItems.has(value) + }, + togglePin (value) { + if (this.isPinned(value)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } + this.$store.dispatch('pushServerSideStorage') + } + }, + computed: { + routeTo () { + if (!this.item.route && !this.item.routeObject) return null + let route + if (this.item.routeObject) { + route = this.item.routeObject + } else { + route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute } + } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name } + } + return route + }, + getters () { + return this.$store.getters + }, + ...mapState({ + currentUser: state => state.users.currentUser, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }) + } +} + +export default NavigationEntry diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue new file mode 100644 index 00000000..f4d53836 --- /dev/null +++ b/src/components/navigation/navigation_entry.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js new file mode 100644 index 00000000..57b8d589 --- /dev/null +++ b/src/components/navigation/navigation_pins.js @@ -0,0 +1,88 @@ +import { mapState } from 'vuex' +import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +) + +const NavPanel = { + props: ['limit'], + methods: { + getRouteTo (item) { + if (item.routeObject) { + return item.routeObject + } + const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name } + } + return route + } + }, + computed: { + getters () { + return this.$store.getters + }, + ...mapState({ + lists: getListEntries, + currentUser: state => state.users.currentUser, + followRequestCount: state => state.api.followRequests.length, + privateMode: state => state.instance.private, + federating: state => state.instance.federating, + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }), + pinnedList () { + if (!this.currentUser) { + return [ + { ...TIMELINES.public, name: 'public' }, + { ...TIMELINES.twkn, name: 'twkn' }, + { ...ROOT_ITEMS.about, name: 'about' } + ] + } + return filterNavigation( + [ + ...Object + .entries({ ...TIMELINES }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })), + ...this.lists.filter((k) => this.pinnedItems.has(k.name)), + ...Object + .entries({ ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + ], + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ).slice(0, this.limit) + } + } +} + +export default NavPanel diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue new file mode 100644 index 00000000..5b3fa6f4 --- /dev/null +++ b/src/components/navigation/navigation_pins.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index fc8affd7..a2713fc7 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -3,8 +3,10 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' -import StatusContent from '../status_content/status_content.vue' +import Report from '../report/report.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' +import UserPopover from '../user_popover/user_popover.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -39,14 +41,17 @@ const Notification = { unmuted: false } }, - props: [ 'notification' ], + props: ['notification'], components: { - StatusContent, + // StatusContent, UserAvatar, UserCard, Timeago, Status, - RichContent + Report, + RichContent, + UserPopover, + UserLink }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 7d3d0c69..26b174ff 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -11,9 +11,10 @@ class="Notification container -muted" > - - {{ notification.from_profile.screen_name_ui }} - +