forked from AkkomaGang/akkoma-fe
Compare commits
91 commits
Author | SHA1 | Date | |
---|---|---|---|
floatingghost | 3e86db24d3 | ||
floatingghost | 7789c5def6 | ||
Floatingghost | f3934afbd8 | ||
Floatingghost | 0b437ab6fd | ||
floatingghost | 8964dce609 | ||
floatingghost | 1a49a1b3ac | ||
Floatingghost | 1adef56603 | ||
Floatingghost | 5aaa605df0 | ||
Floatingghost | 71e287d56c | ||
floatingghost | 70275684bf | ||
floatingghost | 29ff2ef455 | ||
Oneric | 62e0dd858c | ||
sarah | cc709394c5 | ||
Serhii Tereshchenko | 57beea6a0d | ||
Serhii Tereshchenko | 042e8c78dc | ||
Oneric | 0e07d88afa | ||
sarah | 1f5f8665c8 | ||
Oneric | 428ed70b0d | ||
floatingghost | ed0b403c33 | ||
floatingghost | 0f842b300b | ||
floatingghost | 865cb6f96a | ||
Oneric | 050c7df2e6 | ||
Oneric | a77a9e04d9 | ||
leap123 | a57334991e | ||
floatingghost | 8dce31d0ad | ||
Hazel Koehler | ea9ad4d600 | ||
Hazel Koehler | 34e2800f59 | ||
Hazel Koehler | 3d65eccf04 | ||
floatingghost | d304be654f | ||
floatingghost | aee97fa948 | ||
floatingghost | 7da1687f31 | ||
floatingghost | a8f193d4bd | ||
floatingghost | 81c82e11bc | ||
floatingghost | 00cadce5b4 | ||
floatingghost | 40a08f279b | ||
Oneric | c524a47e6f | ||
Ngô Ngọc Đức Huy | 235c734d37 | ||
Ngô Ngọc Đức Huy | deaef1d0b9 | ||
smitten | 1b28ec3b72 | ||
smitten | c9dc8f00f9 | ||
Oneric | beee99e733 | ||
Oneric | ccb0ffdc8a | ||
Oneric | ab250c2f3a | ||
Norm | 1de62fffcd | ||
Norm | 306cea04a1 | ||
Mergan | d9e1bc4d99 | ||
FloatingGhost | 52b0b6f008 | ||
floatingghost | 8afbe5e3bc | ||
floatingghost | 58be48d164 | ||
Mergan | 1056b89fd1 | ||
Mergan | 3e64d78d05 | ||
Mergan | 3947aafeba | ||
Mergan | 345934c2f3 | ||
Mergan | 42a13b0f1b | ||
Mergan | e13c4b6b85 | ||
sn0w | 6a1409e09b | ||
FloatingGhost | 174f98b1cb | ||
FloatingGhost | ab146b67ec | ||
FloatingGhost | 3b4208ea41 | ||
floatingghost | 856324fa26 | ||
floatingghost | 5a9322d2c7 | ||
b52bfbcba0 | |||
floatingghost | 2b05a738c9 | ||
fa5d31b793 | |||
29cfdcbbcd | |||
5174b95918 | |||
56528206b8 | |||
590380c084 | |||
dfcbb3c1ec | |||
9b01c31283 | |||
6be003b2f8 | |||
cc302fb0e2 | |||
c4e768f977 | |||
d8a7217335 | |||
floatingghost | 35d10ab9ff | ||
43d0a24547 | |||
51ebe643d5 | |||
Ngô Ngọc Đức Huy | 7c14e1a5bd | ||
0da0e2c814 | |||
floatingghost | af97dd7484 | ||
floatingghost | 28bf597443 | ||
floatingghost | a249baea8c | ||
floatingghost | a4e82f7886 | ||
32dc55b07c | |||
91eab22d77 | |||
e2125c57d6 | |||
e0a6418e91 | |||
14ed359c33 | |||
Norm | 2d387e2eb4 | ||
Norm | 57f70371a9 | ||
Norm | 8feffbcdf6 |
6
.babelrc
6
.babelrc
|
@ -1,9 +1,5 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
"lodash",
|
||||
"@vue/babel-plugin-jsx"
|
||||
],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||
"comments": false
|
||||
}
|
||||
|
|
|
@ -5,9 +5,14 @@ module.exports = {
|
|||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
|
||||
extends: [
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: ['vue', 'import'],
|
||||
plugins: [
|
||||
'vue',
|
||||
'import'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"singleAttributePerLine": true
|
||||
}
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
|||
nodejs 20.12.2
|
|
@ -1,9 +1,12 @@
|
|||
pipeline:
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
steps:
|
||||
lint:
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
|
@ -13,11 +16,11 @@ pipeline:
|
|||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- apt update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
- yarn
|
||||
- yarn
|
||||
- yarn unit
|
||||
|
||||
build:
|
||||
|
@ -27,7 +30,7 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn build
|
||||
|
@ -39,15 +42,15 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
image: node:20
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- zip akkoma-fe.zip -r dist
|
||||
|
@ -69,8 +72,8 @@ pipeline:
|
|||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget git zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- cd docs
|
||||
|
@ -78,4 +81,4 @@ pipeline:
|
|||
- mkdocs build
|
||||
- zip -r docs.zip site/*
|
||||
- cd site
|
||||
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/
|
||||
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/
|
||||
|
|
10
README.md
10
README.md
|
@ -1,4 +1,4 @@
|
|||
# Akkoma-FE
|
||||
# Akkoma-FE
|
||||
|
||||
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
|
||||
|
||||
|
@ -8,7 +8,7 @@ This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkom
|
|||
|
||||
# For Translators
|
||||
|
||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
|
||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
|
||||
|
||||
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
|
||||
|
||||
|
@ -22,7 +22,7 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
|
|||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install -g yarn
|
||||
corepack enable
|
||||
yarn
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
|
@ -37,7 +37,7 @@ 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:
|
||||
You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/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.
|
||||
|
@ -52,4 +52,4 @@ Edit config.json for configuration.
|
|||
|
||||
### 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.
|
||||
```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.
|
||||
|
|
|
@ -11,17 +11,14 @@ var webpackConfig = require('./webpack.prod.conf')
|
|||
|
||||
console.log(
|
||||
' Tip:\n' +
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
" Opening index.html over file:// won't work.\n"
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
)
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var assetsPath = path.join(
|
||||
config.build.assetsRoot,
|
||||
config.build.assetsSubDirectory
|
||||
)
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
@ -29,13 +26,11 @@ cp('-R', 'static/*', assetsPath)
|
|||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(
|
||||
stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n'
|
||||
)
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
})
|
||||
|
|
|
@ -2,7 +2,8 @@ var semver = require('semver')
|
|||
var chalk = require('chalk')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
|
@ -23,23 +24,16 @@ module.exports = function () {
|
|||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'To use this template, you must update following to modules:'
|
||||
)
|
||||
)
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
|
|
|
@ -6,10 +6,9 @@ var express = require('express')
|
|||
var webpack = require('webpack')
|
||||
var opn = require('opn')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig =
|
||||
process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
|
@ -51,10 +50,7 @@ app.use(devMiddleware)
|
|||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(
|
||||
config.dev.assetsPublicPath,
|
||||
config.dev.assetsSubDirectory
|
||||
)
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
|
|
|
@ -4,8 +4,7 @@ var sass = require('sass')
|
|||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory =
|
||||
process.env.NODE_ENV === 'production'
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
|
@ -14,7 +13,7 @@ exports.assetsPath = function (_path) {
|
|||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
function generateLoaders(loaders) {
|
||||
function generateLoaders (loaders) {
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
|
@ -28,11 +27,11 @@ exports.cssLoaders = function (options) {
|
|||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader'])
|
||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
|
@ -53,8 +52,8 @@ exports.cssLoaders = function (options) {
|
|||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
|
||||
}
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ var { VueLoaderPlugin } = require('vue-loader')
|
|||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
// various preprocessor loaders added to vue-loader at the end of this file
|
||||
var cssSourceMapDev = env === 'development' && config.dev.cssSourceMap
|
||||
var cssSourceMapProd = env === 'production' && config.build.productionSourceMap
|
||||
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
|
||||
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
|
||||
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
||||
|
||||
var now = Date.now()
|
||||
|
@ -18,12 +18,9 @@ module.exports = {
|
|||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
|
||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
||||
path: config.build.assetsRoot,
|
||||
publicPath:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
|
@ -33,15 +30,17 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||
modules: [path.join(__dirname, '../node_modules')],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
fallback: {
|
||||
url: require.resolve('url/')
|
||||
"url": require.resolve("url/"),
|
||||
},
|
||||
alias: {
|
||||
static: path.resolve(__dirname, '../static'),
|
||||
src: path.resolve(__dirname, '../src'),
|
||||
assets: path.resolve(__dirname, '../src/assets'),
|
||||
components: path.resolve(__dirname, '../src/components'),
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
}
|
||||
},
|
||||
|
@ -67,15 +66,14 @@ module.exports = {
|
|||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
type: 'javascript/auto',
|
||||
loader: '@intlify/vue-i18n-loader',
|
||||
include: [
|
||||
// Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: "javascript/auto"
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
|
@ -116,8 +114,10 @@ module.exports = {
|
|||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()]
|
||||
plugins: [
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
|
|||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(
|
||||
baseWebpackConfig.entry[name]
|
||||
)
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
|
@ -22,10 +20,10 @@ module.exports = merge(baseWebpackConfig, {
|
|||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
COMMIT_HASH: JSON.stringify('DEV'),
|
||||
DEV_OVERRIDES: JSON.stringify(config.dev.settings),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
|
|
@ -2,27 +2,23 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
||||
var { merge } = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env =
|
||||
process.env.NODE_ENV === 'testing'
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
let commitHash = require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.dev.cssSourceMap,
|
||||
extract: true
|
||||
})
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
|
@ -40,15 +36,15 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
}),
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
COMMIT_HASH: JSON.stringify(commitHash),
|
||||
DEV_OVERRIDES: JSON.stringify(undefined),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// extract css into its own file
|
||||
new MiniCssExtractPlugin({
|
||||
|
@ -58,8 +54,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename:
|
||||
process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
|
@ -72,7 +69,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'auto'
|
||||
})
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
|
@ -90,7 +87,9 @@ if (config.build.productionGzip) {
|
|||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
|
|
|
@ -38,7 +38,7 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
|
@ -59,7 +59,7 @@ module.exports = {
|
|||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
Origin: target
|
||||
'Origin': target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
|
@ -76,7 +76,7 @@ module.exports = {
|
|||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
}
|
||||
},
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"target": "https://pleroma.soykaf.com/",
|
||||
"target": "https://otp.akkoma.dev/",
|
||||
"staticConfigPreference": false
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# Pleroma-FE configuration and customization for instance administrators
|
||||
# Akkoma-FE configuration and customization for instance administrators
|
||||
|
||||
* *For user configuration, see [Pleroma-FE user guide](../user_guide)*
|
||||
* *For user configuration, see [Akkoma-FE user guide](../user_guide)*
|
||||
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
|
||||
|
||||
## Where configuration is stored
|
||||
|
||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||
Akkoma-FE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/modules/instance.js) )
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, Akkoma-FE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/src/modules/instance.js) )
|
||||
|
||||
## Instance-defaults
|
||||
|
||||
|
@ -59,7 +59,7 @@ Instance `logo`, could be any image, including svg. By default it assumes logo u
|
|||
`logoMargin` allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.
|
||||
|
||||
### `minimalScopesMode`
|
||||
Limit scope selection to *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 PleromaFE.
|
||||
Limit scope selection to *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 Akkoma-FE.
|
||||
|
||||
### `nsfwCensorImage`
|
||||
Use custom image for NSFW'd images
|
||||
|
@ -77,7 +77,7 @@ Change alignment of sidebar and panels to the right. Defaults to `false`.
|
|||
Show panel showcasing instance features/settings to logged-out visitors
|
||||
|
||||
### `showInstanceSpecificPanel`
|
||||
This allows you to include arbitrary HTML content in a panel below navigation menu. PleromaFE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel.
|
||||
This allows you to include arbitrary HTML content in a panel below navigation menu. Akkoma-FE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel.
|
||||
|
||||
### `subjectLineBehavior`
|
||||
How to handle subject line (CW) when replying to a post.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Hacking, tweaking, contributing
|
||||
|
||||
## What PleromaFE even is, how it works
|
||||
## What Akkoma-FE even is, how it works
|
||||
|
||||
PleromaFE is an SPA (Single-Page Application) backed by [Vue](https://vuejs.org/) framework. It means that it's just a nearly-empty HTML page with bunch of JavaScript that actually generates and controls DOM (i.e. html elements) in Runtime. Currently, there's no way around it - you have to have Javascript enabled in the browser to make it work, there is a theoretical possibility to generate some HTML server-side but it's not implemented yet.
|
||||
Akkoma-FE is an SPA (Single-Page Application) backed by [Vue](https://vuejs.org/) framework. It means that it's just a nearly-empty HTML page with bunch of JavaScript that actually generates and controls DOM (i.e. html elements) in Runtime. Currently, there's no way around it - you have to have Javascript enabled in the browser to make it work, there is a theoretical possibility to generate some HTML server-side but it's not implemented yet.
|
||||
|
||||
You can serve static html page and everything from any HTTP(S) server but currently it will try to access /api/ path at same domain it's running on, meaning that as of right now you cannot put it on one domain and access the other without proxying requests.
|
||||
|
||||
|
@ -67,19 +67,19 @@ server {
|
|||
|
||||
### API, Data, Operations
|
||||
|
||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
In 99% cases Akkoma-FE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
Akkoma-FE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
|
||||
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
||||
For most part, Akkoma-FE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
||||
|
||||
PleromaFE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB.
|
||||
Akkoma-FE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB.
|
||||
|
||||
TODO: Refactor API code and document it here
|
||||
|
||||
### Themes
|
||||
|
||||
PleromaFE uses custom theme "framework" which is pretty much just a style tag rendered by vue which only contains CSS3 variables. Every color used in UI should be derived from theme. Theme is stored in a JSON object containing color, opacity, shadow and font information, with most of it being optional.
|
||||
Akkoma-FE uses custom theme "framework" which is pretty much just a style tag rendered by vue which only contains CSS3 variables. Every color used in UI should be derived from theme. Theme is stored in a JSON object containing color, opacity, shadow and font information, with most of it being optional.
|
||||
|
||||
The most basic theme can consist of 4 to 8 "basic colors", which is also what previous version of themes allowed, with all other colors being derived from those basic colors, i.e. "light background" will be "background" color lightened/darkened, "panel header" will be same as "foreground". The idea is that you can specify just basic color palette and everything else will be generated automatically, but if you really need to tweak some specific color - you can.
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 1.4 KiB |
BIN
docs/docs/images/akkoma_logo_vector_bg_32.png
Normal file
BIN
docs/docs/images/akkoma_logo_vector_bg_32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
24
docs/docs/images/akkoma_logo_vector_nobg.svg
Normal file
24
docs/docs/images/akkoma_logo_vector_nobg.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.83 362.83">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1, .cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke: #fff;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_6" data-name="Layer 6">
|
||||
<path class="cls-1" d="M115.2,131.89c6.26-6.54,20.19-20.63,42.39-26.14,15.79-3.92,28.51-1.28,33.51,0,83.72,21.41,116.03,201.78,77.79,226.32-10.28,6.6-26.86,2.7-36.77-3.3-32.63-19.78-29.3-72.87-44.44-73.73-5.11-.29-7.15,5.8-20.91,24.94-19.63,27.3-31.49,43.44-49.21,50.87-2.53,1.06-26.91,12.07-41.84,1.23-38.55-28-2.96-155.84,39.49-200.18Zm56.31,10.45c-27.39-.52-46.38,38.21-37.98,54.55,10.09,19.62,65.5,18.26,74.77-3.3,7.21-16.78-11.38-50.77-36.79-51.24Z"/>
|
||||
</g>
|
||||
<g id="Layer_4" data-name="Layer 4">
|
||||
<path class="cls-1" d="M68.93,86.51c-6.55,27.74,252.45,113.97,267.56,89.66,9.24-14.87-64.9-83.62-163.53-97.57-39.06-5.52-100.95-5.14-104.03,7.91Z"/>
|
||||
</g>
|
||||
<g id="Layer_5" data-name="Layer 5">
|
||||
<path class="cls-2" d="M138.96,93.76c.41-5.25,6.51-5.74,28.85-19.42,26.97-16.51,28.85-22.38,56.86-40.83,30.07-19.81,48.46-31.94,54.82-26.61,9.72,8.15-25.18,43.33-21.31,99.35,.87,12.61,3.12,17.79-.86,23.01-18.25,23.95-120.07-13.68-118.35-35.5Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 530 B |
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg4485"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
sodipodi:docname="pleroma_logo_vector_nobg.svg"
|
||||
inkscape:version="0.92.1 r15371">
|
||||
<metadata
|
||||
id="metadata4491">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4489" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="997"
|
||||
id="namedview4487"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.70710678"
|
||||
inkscape:cx="26.131594"
|
||||
inkscape:cy="235.37499"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="22"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4485" />
|
||||
<g
|
||||
id="g4612">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4495"
|
||||
d="M 235,89 V 423 H 152 V 115 l 26,-26 z"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="178"
|
||||
cy="115"
|
||||
id="path4497"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="335"
|
||||
cy="230"
|
||||
id="path4497-0"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4516"
|
||||
d="M 277,256 V 89 l 84,3e-6 L 361.00002,230 335,256 Z"
|
||||
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="335"
|
||||
cy="397"
|
||||
id="path4497-0-6"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4516-5"
|
||||
d="m 277,423 v -83 h 84 l 2e-5,57 L 335,423 Z"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
|
@ -1,8 +1,8 @@
|
|||
# Introduction to Pleroma-FE
|
||||
## What is Pleroma-FE?
|
||||
# Introduction to Akkoma-FE
|
||||
## What is Akkoma-FE?
|
||||
|
||||
Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
||||
Akkoma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
||||
|
||||
## How can I use it?
|
||||
|
||||
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
||||
If your instance uses Akkoma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Akkoma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Adding stickers
|
||||
|
||||
Pleroma-fe supports stickers, which are essentially little images stored server-side
|
||||
Akkoma-FE supports stickers, which are essentially little images stored server-side
|
||||
which can be selected by a user to automatically attach them to a post.
|
||||
|
||||
There's no explicit setting for these, they just rely on the existence of certain files.
|
||||
|
||||
## Initialising the sticker config file
|
||||
|
||||
You're probably serving pleroma-fe from your instance's `instance/static/` directory -
|
||||
You're probably serving Akkoma-FE from your instance's `instance/static/` directory -
|
||||
this directy can also override files served at a given path.
|
||||
|
||||
The first thing we need to do is set up our `stickers.json` file. At `instance/static/static/stickers.json`,
|
||||
|
@ -50,4 +50,4 @@ The `tabIcon` will appear on the sticker picker itself as a representative of th
|
|||
|
||||
You can add as many stickers as you like. They should all be in the same directory as your `pack.json`.
|
||||
|
||||
Now you should find that there's a sticky note icon on the emoji picker on pleroma-fe that allows you to attach stickers.
|
||||
Now you should find that there's a sticky note icon on the emoji picker on Akkoma-FE that allows you to attach stickers.
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
>
|
||||
> --Catbag
|
||||
|
||||
Pleroma-FE is the default user-facing frontend for Pleroma. If your instance uses Pleroma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa.
|
||||
Akkoma-FE is the default user-facing frontend for Pleroma. If your instance uses Akkoma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa.
|
||||
|
||||
### Left column
|
||||
|
||||
- first block: This section is dedicated to [posting](posting_reading_basic_functions.md)
|
||||
- second block: Here you can switch between the different views for the right column.
|
||||
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Pleroma-FE and Mastodon-FE.
|
||||
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Akkoma-FE and Mastodon-FE.
|
||||
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses
|
||||
|
||||
### Right column
|
||||
|
|
|
@ -15,13 +15,13 @@ Posts will contain the text you are posting, but some content will be modified:
|
|||
Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line...
|
||||
|
||||
**Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are a more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist.
|
||||
Let's say there's a `:pleroma:` emoji defined on an instance. That means
|
||||
> First time using :pleroma: pleroma!
|
||||
Let's say there's a `:akkoma:` emoji defined on an instance. That means
|
||||
> First time using :akkoma: akkoma!
|
||||
|
||||
will become
|
||||
> First time using ![pleroma](../assets/example_emoji.png) pleroma!
|
||||
will become
|
||||
> First time using ![akkoma](../assets/example_emoji.png) akkoma!
|
||||
|
||||
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
|
||||
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
|
||||
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoji and complete the shortcode for you if you select one. If emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
|
||||
|
||||
**Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add the `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
|
||||
|
@ -42,7 +42,7 @@ A few things to consider about the security and usage of these scopes:
|
|||
- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned.
|
||||
* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to.
|
||||
|
||||
Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug.
|
||||
Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in Akkoma-FE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug.
|
||||
|
||||
## Rich text
|
||||
|
||||
|
@ -51,7 +51,7 @@ By default new posts you make are plaintext, meaning you can't make text **bold*
|
|||
Here is a small example of some text in markdown.
|
||||
|
||||
```
|
||||
This is an example of markdown text using **bold** and *cursive* text.
|
||||
This is an example of markdown text using **bold** and *cursive* text.
|
||||
To get a newline we add two spaces at the end of the previous line.
|
||||
|
||||
Let's also add a list
|
||||
|
@ -67,7 +67,7 @@ If you set the input-method to Markdown, and post this, it will look something l
|
|||
|
||||
## Misskey markdown
|
||||
|
||||
The akkoma version of pleroma-fe includes support for writing and rendering
|
||||
Akkoma-FE includes support for writing and rendering
|
||||
misskey markdown (MFM). To write this you will need to select "MFM" from
|
||||
the content type dropdown (if supported), and then you can format text
|
||||
[in MFM](https://akkoma.dev/sfr/marked-mfm/src/branch/master/docs/syntax.md).
|
||||
|
|
|
@ -83,7 +83,7 @@ Here you can change your password, revoke access tokens, configure 2-factor auth
|
|||
|
||||
## Theme
|
||||
|
||||
Here you can change the look and feel of Pleroma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section.
|
||||
Here you can change the look and feel of Akkoma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section.
|
||||
|
||||
The themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ When you see someone, you can click on their user picture to view their profile,
|
|||
|
||||
**Following** is self-explanatory, it adds them to your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any.
|
||||
|
||||
**Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than PleromaFE may completely remove their posts.
|
||||
**Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than Akkoma-FE may completely remove their posts.
|
||||
|
||||
**Blocking** a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you).
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
site_name: Pleroma-FE Documentation
|
||||
site_name: Akkoma-FE Documentation
|
||||
theme:
|
||||
favicon: 'images/pleroma_logo_vector_bg_32.png'
|
||||
favicon: 'images/akkoma_logo_vector_bg_32.png'
|
||||
name: 'material'
|
||||
custom_dir: 'theme'
|
||||
# Disable google fonts
|
||||
font: false
|
||||
logo: 'images/pleroma_logo_vector_nobg.svg'
|
||||
logo: 'images/akkoma_logo_vector_nobg.svg'
|
||||
features:
|
||||
- tabs
|
||||
palette:
|
||||
|
@ -14,8 +14,8 @@ theme:
|
|||
|
||||
extra_css:
|
||||
- css/extra.css
|
||||
repo_name: 'AkkomaGang/pleroma-fe'
|
||||
repo_url: 'https://akkoma.dev/AkkomaGang/pleroma-fe'
|
||||
repo_name: 'AkkomaGang/akkoma-fe'
|
||||
repo_url: 'https://akkoma.dev/AkkomaGang/akkoma-fe'
|
||||
|
||||
extra:
|
||||
repo_icon: gitea
|
||||
|
|
8
docs/theme/partials/source.html
vendored
8
docs/theme/partials/source.html
vendored
|
@ -38,11 +38,11 @@
|
|||
{% endif %}
|
||||
|
||||
{% if page and page.url.startswith('backend') %}
|
||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
|
||||
{% set repo_name = "pleroma/pleroma" %}
|
||||
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma" %}
|
||||
{% set repo_name = "AkkomaGang/akkoma" %}
|
||||
{% elif page and page.url.startswith('frontend') %}
|
||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
|
||||
{% set repo_name = "pleroma/pleroma-fe" %}
|
||||
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma-fe" %}
|
||||
{% set repo_name = "AkkomaGang/akkoma-fe" %}
|
||||
{% else %}
|
||||
{% set repo_url = config.repo_url %}
|
||||
{% set repo_name = config.repo_name %}
|
||||
|
|
41
index.html
41
index.html
|
@ -1,25 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no" />
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||
<link rel="stylesheet" href="/static/mfm.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
</head>
|
||||
|
||||
<body class="hidden">
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||
<link rel="stylesheet" href="/static/mfm.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body class="hidden">
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.5.0",
|
||||
"version": "3.10.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
|
@ -58,7 +58,7 @@
|
|||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"autoprefixer": "6.7.7",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
|
@ -69,13 +69,11 @@
|
|||
"css-loader": "^6.7.2",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
|
@ -104,11 +102,9 @@
|
|||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"prettier": "2.8.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: [require('autoprefixer')]
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"]
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
|
|
86
src/App.js
86
src/App.js
|
@ -24,9 +24,7 @@ export default {
|
|||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() =>
|
||||
import('./components/notifications/notifications.vue')
|
||||
),
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -46,20 +44,17 @@ export default {
|
|||
data: () => ({
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
created() {
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'interfaceLanguage',
|
||||
value: val
|
||||
})
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
computed: {
|
||||
classes() {
|
||||
classes () {
|
||||
return [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
|
@ -69,76 +64,53 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
pageBackground () {
|
||||
return this.mergedConfig.displayPageBackgrounds
|
||||
? this.$store.state.users.displayBackground
|
||||
: null
|
||||
},
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
instanceBackground() {
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
}
|
||||
},
|
||||
suggestionsEnabled() {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
newPostButtonShown() {
|
||||
return (
|
||||
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
newPostButtonShown () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
},
|
||||
layoutType() {
|
||||
return this.$store.state.interface.layoutType
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
this.$store.getters.mergedConfig
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky() {
|
||||
return this.$store.getters.mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return this.$store.getters.mergedConfig.showScrollbars
|
||||
},
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
updateMobileState() {
|
||||
updateMobileState () {
|
||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||
}
|
||||
|
|
133
src/App.scss
133
src/App.scss
|
@ -8,13 +8,13 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $system-sans-serif;
|
||||
font-family: var(--interfaceFont, $system-sans-serif);
|
||||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
margin: 0;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
|
@ -23,13 +23,84 @@ body {
|
|||
overscroll-behavior-y: none;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
background: var(--bg);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ## Custom scrollbars
|
||||
// Only show custom scrollbars on devices which
|
||||
// have a cursor/pointer to operate them
|
||||
@media (any-pointer: fine) {
|
||||
* {
|
||||
scrollbar-color: var(--btn) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--btn);
|
||||
box-shadow: var(--buttonShadow);
|
||||
border-radius: var(--btnRadius);
|
||||
}
|
||||
|
||||
// horizontal/vertical/increment/decrement are webkit-specific stuff
|
||||
// that indicates whether we're affecting vertical scrollbar, increase button etc
|
||||
// stylelint-disable selector-pseudo-class-no-unknown
|
||||
&::-webkit-scrollbar-button {
|
||||
--___bgPadding: 2px;
|
||||
|
||||
color: var(--btnText);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
|
||||
&:horizontal {
|
||||
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
|
||||
}
|
||||
}
|
||||
|
||||
&:vertical {
|
||||
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// stylelint-enable selector-pseudo-class-no-unknown
|
||||
}
|
||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||
html {
|
||||
scrollbar-color: var(--selectedMenu) var(--wallpaper);
|
||||
background: var(--wallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $fallback--link;
|
||||
|
@ -40,7 +111,7 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
i[class*='icon-'],
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
|
@ -58,7 +129,6 @@ nav {
|
|||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
position: fixed;
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
|
@ -113,7 +183,7 @@ nav {
|
|||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
||||
grid-template-areas: 'sidebar content';
|
||||
grid-template-areas: "sidebar content";
|
||||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
|
@ -122,7 +192,6 @@ nav {
|
|||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
padding: 0 calc(var(--columnGap) / 2);
|
||||
|
||||
.column {
|
||||
--___columnMargin: var(--columnGap);
|
||||
|
@ -160,9 +229,7 @@ nav {
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
|
||||
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
|
||||
// might implement old style of hiding scrollbars later if there's demand
|
||||
|
@ -170,9 +237,7 @@ nav {
|
|||
&:not(.-show-scrollbar) {
|
||||
scrollbar-width: none;
|
||||
margin-right: calc(var(--___paddingIncrease) * -1);
|
||||
padding-right: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
|
@ -212,21 +277,21 @@ nav {
|
|||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: 'content sidebar';
|
||||
grid-template-areas: "content sidebar";
|
||||
}
|
||||
|
||||
&.-wide {
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: 'sidebar content notifs';
|
||||
grid-template-areas: "sidebar content notifs";
|
||||
|
||||
&.-reverse {
|
||||
grid-template-areas: 'notifs content sidebar';
|
||||
grid-template-areas: "notifs content sidebar";
|
||||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
grid-template-columns: 100vw;
|
||||
grid-template-areas: 'content';
|
||||
grid-template-areas: "content";
|
||||
padding: 0;
|
||||
|
||||
.column {
|
||||
|
@ -283,7 +348,7 @@ nav {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*='icon-'],
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
@ -299,9 +364,7 @@ nav {
|
|||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
|
@ -334,9 +397,7 @@ nav {
|
|||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
|
||||
svg,
|
||||
|
@ -401,8 +462,7 @@ textarea,
|
|||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -420,13 +480,13 @@ textarea,
|
|||
padding: 0 var(--_padding);
|
||||
|
||||
&:disabled,
|
||||
&[disabled='disabled'],
|
||||
&[disabled=disabled],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[type='range'] {
|
||||
&[type=range] {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
@ -434,13 +494,12 @@ textarea,
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
&[type='radio'] {
|
||||
&[type=radio] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset,
|
||||
0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
|
||||
|
@ -461,7 +520,7 @@ textarea,
|
|||
height: 1.1em;
|
||||
border-radius: 100%; // Radio buttons should always be circle
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -475,7 +534,7 @@ textarea,
|
|||
}
|
||||
}
|
||||
|
||||
&[type='checkbox'] {
|
||||
&[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
@ -501,7 +560,7 @@ textarea,
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -536,8 +595,8 @@ option {
|
|||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
:to="{ name: 'login' }"
|
||||
class="panel-body"
|
||||
>
|
||||
{{ $t('login.hint') }}
|
||||
{{ $t("login.hint") }}
|
||||
</router-link>
|
||||
</div>
|
||||
<router-view />
|
||||
|
|
|
@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
|
|||
|
||||
$fallback--bg: #121a24;
|
||||
$fallback--fg: #182230;
|
||||
$fallback--faint: rgba(185, 185, 186, 0.5);
|
||||
$fallback--faint: rgba(185, 185, 186, .5);
|
||||
$fallback--text: #b9b9ba;
|
||||
$fallback--link: #d8a070;
|
||||
$fallback--icon: #666;
|
||||
|
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
|
|||
$fallback--cGreen: #0fa00f;
|
||||
$fallback--cOrange: orange;
|
||||
|
||||
$fallback--alertError: rgba(211, 16, 20, 0.5);
|
||||
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
|
@ -28,14 +28,6 @@ $fallback--avatarRadius: 4px;
|
|||
$fallback--avatarAltRadius: 10px;
|
||||
$fallback--attachmentRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 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;
|
||||
$fallback--buttonShadow: 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;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
|
||||
sans-serif;
|
||||
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
|
||||
monospace;
|
||||
|
|
|
@ -3,21 +3,15 @@ import { createApp } from 'vue'
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
|
||||
import {
|
||||
windowWidth,
|
||||
windowHeight
|
||||
} from '../services/window_utils/window_utils'
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
|
@ -31,9 +25,7 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent
|
||||
)
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
@ -81,30 +73,18 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'textlimit',
|
||||
value: textlimit
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountApprovalRequired',
|
||||
value: data.approval_required
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
// don't override cookie if set
|
||||
if (!Cookies.get('userLanguage')) {
|
||||
store.dispatch('setOption', {
|
||||
name: 'interfaceLanguage',
|
||||
value: resolveLanguage(data.languages)
|
||||
})
|
||||
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
|
||||
}
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'vapidPublicKey',
|
||||
value: vapidPublicKey
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
|
@ -119,12 +99,10 @@ const getBackendProvidedConfig = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal'
|
||||
)
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +113,7 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
|
@ -178,12 +156,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined' ? true : config.logoMask
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
@ -211,7 +193,7 @@ const getTOS = async ({ store }) => {
|
|||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS")
|
||||
|
@ -224,12 +206,9 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'instanceSpecificPanelContent',
|
||||
value: html
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel")
|
||||
|
@ -242,30 +221,25 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)
|
||||
).sort((a, b) => {
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'stickers',
|
||||
value: stickers
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers")
|
||||
|
@ -280,19 +254,13 @@ const getAppSecret = async ({ store }) => {
|
|||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(store.getters.getToken())
|
||||
)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'staffAccounts',
|
||||
value: nicknames
|
||||
})
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
|
@ -302,146 +270,68 @@ const getNodeInfo = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'name',
|
||||
value: metadata.nodeName
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'registrationOpen',
|
||||
value: data.openRegistrations
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'mediaProxyAvailable',
|
||||
value: features.includes('media_proxy')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'safeDM',
|
||||
value: features.includes('safe_dm_mentions')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pollsAvailable',
|
||||
value: features.includes('polls')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'editingAvailable',
|
||||
value: features.includes('editing')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pollLimits',
|
||||
value: metadata.pollLimits
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'mailerEnabled',
|
||||
value: metadata.mailerEnabled
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'translationEnabled',
|
||||
value: features.includes('akkoma:machine_translation')
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'uploadlimit',
|
||||
value: parseInt(uploadLimits.general)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backgroundlimit',
|
||||
value: parseInt(uploadLimits.background)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'bannerlimit',
|
||||
value: parseInt(uploadLimits.banner)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'fieldsLimits',
|
||||
value: metadata.fieldsLimits
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'postFormats',
|
||||
value: metadata.postFormats
|
||||
})
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaBackend',
|
||||
value: software.name === 'pleroma'
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'frontendVersion',
|
||||
value: frontendVersion
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value:
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federationPolicy',
|
||||
value: federation
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'publicTimelineVisibility',
|
||||
value: metadata.publicTimelineVisibility
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federatedTimelineAvailable',
|
||||
value: metadata.federatedTimelineAvailable
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'publicTimelineVisibility', value: metadata.publicTimelineVisibility })
|
||||
store.dispatch('setInstanceOption', { name: 'federatedTimelineAvailable', value: metadata.federatedTimelineAvailable })
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountActivationRequired',
|
||||
value: accountActivationRequired
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
|
@ -451,16 +341,11 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig()
|
||||
])
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(
|
||||
getAppSecret({ store })
|
||||
)
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
|
@ -483,10 +368,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
FaviconService.initFaviconService()
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
|
@ -496,10 +378,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (
|
||||
customThemeSource &&
|
||||
customThemeSource.themeEngineVersion === CURRENT_VERSION
|
||||
) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
|
@ -527,7 +406,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,145 +35,51 @@ export default (store) => {
|
|||
}
|
||||
|
||||
let routes = [
|
||||
{
|
||||
name: 'root',
|
||||
{ name: 'root',
|
||||
path: '/',
|
||||
redirect: (_to) => {
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
)
|
||||
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: 'bubble-timeline',
|
||||
path: '/main/bubble',
|
||||
component: BubbleTimeline
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true }
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
{ name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/:id',
|
||||
component: UserProfile,
|
||||
meta: { dontScroll: true }
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{
|
||||
name: 'registration-request-sent',
|
||||
path: '/registration-request-sent',
|
||||
component: RegistrationRequestSent
|
||||
},
|
||||
{
|
||||
name: 'awaiting-email-confirmation',
|
||||
path: '/awaiting-email-confirmation',
|
||||
component: AwaitingEmailConfirmation
|
||||
},
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
|
||||
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
props: (route) => ({ code: route.query.code })
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query })
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage
|
||||
},
|
||||
{
|
||||
name: 'user-profile',
|
||||
path: '/:_(users)?/:name',
|
||||
component: UserProfile,
|
||||
meta: { dontScroll: true }
|
||||
}
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
|
||||
]
|
||||
|
||||
return routes
|
||||
|
|
|
@ -15,17 +15,13 @@ const About = {
|
|||
LocalBubblePanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
showLocalBubblePanel() {
|
||||
showLocalBubblePanel () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
<script src="./about.js" ></script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
@ -3,13 +3,19 @@ import Popover from '../popover/popover.vue'
|
|||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { mapState } from 'vuex'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
|
||||
const AccountActions = {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingConfirmBlock: false
|
||||
}
|
||||
|
@ -20,59 +26,56 @@ const AccountActions = {
|
|||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
refetchRelationship() {
|
||||
refetchRelationship () {
|
||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
showConfirmBlock() {
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock() {
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats() {
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats() {
|
||||
hideRepeats () {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser() {
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser() {
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser() {
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers() {
|
||||
removeUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser() {
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
muteDomain() {
|
||||
this.$store
|
||||
.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||
muteDomain () {
|
||||
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
},
|
||||
unmuteDomain() {
|
||||
this.$store
|
||||
.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||
unmuteDomain () {
|
||||
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock() {
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: (state) =>
|
||||
state.instance.pleromaChatMessagesAvailable
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
<template v-slot:content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
|
@ -71,7 +71,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
@ -93,8 +93,10 @@
|
|||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
<template v-slot:user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -8,7 +8,7 @@ const Announcement = {
|
|||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
|
@ -25,93 +25,78 @@ const Announcement = {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
content() {
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead() {
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt() {
|
||||
publishedAt () {
|
||||
const time = this.announcement['published_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt() {
|
||||
startsAt () {
|
||||
const time = this.announcement['starts_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt() {
|
||||
endsAt () {
|
||||
const time = this.announcement['ends_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive() {
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead() {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch(
|
||||
'markAnnouncementAsRead',
|
||||
this.announcement.id
|
||||
)
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement() {
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate(time, locale) {
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement['all_day']
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode() {
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
||||
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
||||
this.editedAnnouncement.allDay = this.announcement['all_day']
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit() {
|
||||
this.$store
|
||||
.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit() {
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{
|
||||
$t('announcements.published_time_display', { time: publishedAt })
|
||||
}}
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
|
@ -101,7 +99,7 @@
|
|||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
|
@ -110,8 +108,7 @@
|
|||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading,
|
||||
.body {
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,26 +10,22 @@
|
|||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{
|
||||
$t('announcements.start_time_prompt')
|
||||
}}</label>
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{
|
||||
$t('announcements.end_time_prompt')
|
||||
}}</label>
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
|
@ -37,9 +33,7 @@
|
|||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{
|
||||
$t('announcements.all_day_prompt')
|
||||
}}</label>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@ const AnnouncementsPage = {
|
|||
Announcement,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
|
@ -19,35 +19,34 @@ const AnnouncementsPage = {
|
|||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements() {
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement() {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
this.$store
|
||||
.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section v-if="currentUser && currentUser.role === 'admin'">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
>
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
|
@ -48,7 +50,9 @@
|
|||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement :announcement="announcement" />
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,14 +61,13 @@
|
|||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
@import "../../variables";
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading,
|
||||
.body {
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry() {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ export default {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
.btn {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -47,16 +47,14 @@ const Attachment = {
|
|||
'shiftDn',
|
||||
'edit'
|
||||
],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img:
|
||||
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
|
||||
document.createElement('img'),
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
|
@ -70,7 +68,7 @@ const Attachment = {
|
|||
Blurhash
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
|
@ -82,40 +80,40 @@ const Attachment = {
|
|||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder() {
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit() {
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
useBlurhash() {
|
||||
useBlurhash () {
|
||||
return this.$store.getters.mergedConfig.useBlurhash
|
||||
},
|
||||
placeholderName() {
|
||||
placeholderName () {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass() {
|
||||
placeholderIconClass () {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy() {
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type() {
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden() {
|
||||
hidden () {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty() {
|
||||
return this.type === 'html' && !this.attachment.oembed
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
},
|
||||
useModal() {
|
||||
useModal () {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
@ -130,29 +128,29 @@ const Attachment = {
|
|||
}
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag() {
|
||||
videoTag () {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
statusForm() {
|
||||
statusForm () {
|
||||
return this.$parent.$parent
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description'(newVal) {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription(newVal) {
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked({ target }) {
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal(event) {
|
||||
openModal (event) {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
|
@ -160,35 +158,34 @@ const Attachment = {
|
|||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce(event) {
|
||||
openModalForce (event) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
},
|
||||
onEdit(event) {
|
||||
onEdit (event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove() {
|
||||
onRemove () {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp() {
|
||||
onShiftUp () {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn() {
|
||||
onShiftDn () {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash() {
|
||||
stopFlash () {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded(event) {
|
||||
setFlashLoaded (event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription() {
|
||||
toggleDescription () {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden(event) {
|
||||
toggleHidden (event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
|
@ -209,16 +206,14 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad(image) {
|
||||
onImageLoad (image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
resize(e) {
|
||||
resize (e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) {
|
||||
return
|
||||
}
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
|
||||
// Reset to default height for empty form, nothing else to do here.
|
||||
if (target.value === '') {
|
||||
|
@ -229,16 +224,14 @@ const Attachment = {
|
|||
|
||||
const paddingString = getComputedStyle(target)['padding']
|
||||
// remove -px suffix
|
||||
const padding = Number(
|
||||
paddingString.substring(0, paddingString.length - 2)
|
||||
)
|
||||
const padding = Number(paddingString.substring(0, paddingString.length - 2))
|
||||
|
||||
target.style.height = 'auto'
|
||||
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
||||
target.style.height = `${newHeight}px`
|
||||
this.$emit('resize', newHeight)
|
||||
},
|
||||
postStatus(event) {
|
||||
postStatus (event) {
|
||||
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
|
@ -117,6 +117,7 @@
|
|||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
@click.prevent
|
||||
>
|
||||
<FAIcon :icon="placeholderIconClass" />
|
||||
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
|
||||
>{{ edit ? '' : placeholderName }}
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||
</a>
|
||||
<div
|
||||
v-if="edit || remove"
|
||||
|
@ -31,11 +30,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || localDescription || showDescription)
|
||||
"
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
@ -46,7 +41,7 @@
|
|||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
/>
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
|
@ -81,7 +76,7 @@
|
|||
:key="nsfwImage"
|
||||
class="nsfw"
|
||||
:src="nsfwImage"
|
||||
/>
|
||||
>
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
|
@ -101,12 +96,7 @@
|
|||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
attachment.description &&
|
||||
size !== 'small' &&
|
||||
!edit &&
|
||||
type !== 'unknown'
|
||||
"
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
|
@ -158,7 +148,7 @@
|
|||
<a
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{ '-hidden': hidden && preloadImage }"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
@click.stop.prevent="openModal"
|
||||
|
@ -236,13 +226,11 @@
|
|||
v-if="attachment.thumb_url"
|
||||
class="image"
|
||||
>
|
||||
<img :src="attachment.thumb_url" />
|
||||
<img :src="attachment.thumb_url">
|
||||
</div>
|
||||
<div class="text">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1>
|
||||
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
|
||||
</h1>
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
@ -264,11 +252,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || (localDescription && showDescription))
|
||||
"
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
|
|
@ -6,17 +6,13 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render() {
|
||||
render () {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
|
|
|
@ -2,13 +2,11 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
query: { // function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
|
@ -16,7 +14,7 @@ export default {
|
|||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
|
@ -25,31 +23,29 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
filtered() {
|
||||
filtered () {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
term(val) {
|
||||
term (val) {
|
||||
this.fetchResults(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchResults(term) {
|
||||
fetchResults (term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick() {
|
||||
onInputClick () {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside() {
|
||||
onClickOutside () {
|
||||
this.resultsVisible = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:placeholder="placeholder"
|
||||
class="autosuggest-input"
|
||||
@click="onInputClick"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
v-if="resultsVisible && filtered.length > 0"
|
||||
class="autosuggest-results"
|
||||
|
|
|
@ -4,7 +4,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers() {
|
||||
slicedUsers () {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
}
|
||||
},
|
||||
|
@ -12,12 +12,8 @@ const AvatarList = {
|
|||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./avatar_list.js"></script>
|
||||
<script src="./avatar_list.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export default {
|
||||
computed: {}
|
||||
computed: {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: ['user'],
|
||||
data() {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
userExpanded: false
|
||||
}
|
||||
|
@ -16,15 +18,11 @@ const BasicUserCard = {
|
|||
RichContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded() {
|
||||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship() {
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked() {
|
||||
blocked () {
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
|
@ -22,13 +22,13 @@ const BlockCard = {
|
|||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
unblockUser() {
|
||||
unblockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
blockUser() {
|
||||
blockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
|
|
|
@ -6,59 +6,61 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { decode } from 'blurhash'
|
||||
import { decode } from "blurhash";
|
||||
|
||||
export default {
|
||||
name: 'Blurhash',
|
||||
props: {
|
||||
hash: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
punch: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvas: null,
|
||||
ctx: null
|
||||
}
|
||||
ctx: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.canvas = this.$refs.canvas
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
this.canvas.width = 1024
|
||||
this.canvas.height = 512
|
||||
this.draw()
|
||||
this.canvas = this.$refs.canvas;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.canvas.width = 1024;
|
||||
this.canvas.height = 512;
|
||||
this.draw();
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
const pixels = decode(this.hash, this.width, this.height, this.punch)
|
||||
const imageData = this.ctx.createImageData(this.width, this.height)
|
||||
imageData.data.set(pixels)
|
||||
this.ctx.putImageData(imageData, 0, 0)
|
||||
fetch('/static/blurhash-overlay.png')
|
||||
const pixels = decode(this.hash, this.width, this.height, this.punch);
|
||||
const imageData = this.ctx.createImageData(this.width, this.height);
|
||||
imageData.data.set(pixels);
|
||||
this.ctx.putImageData(imageData, 0, 0);
|
||||
fetch("/static/blurhash-overlay.png")
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const img = new Image()
|
||||
img.src = URL.createObjectURL(blob)
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(blob);
|
||||
img.onload = () => {
|
||||
this.ctx.drawImage(img, 0, 0, this.width, this.height)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.ctx.drawImage(img, 0, 0, this.width, this.height);
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -2,14 +2,14 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const Bookmarks = {
|
||||
computed: {
|
||||
timeline() {
|
||||
timeline () {
|
||||
return this.$store.state.statuses.timelines.bookmarks
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,15 @@ const PublicTimeline = {
|
|||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bubble
|
||||
}
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PublicTimeline
|
||||
|
|
|
@ -11,17 +11,14 @@ export default {
|
|||
name: 'Timeago',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
displayDate() {
|
||||
displayDate () {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
{ day: 'numeric', month: 'long' }
|
||||
)
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:checked="modelValue"
|
||||
:indeterminate="indeterminate"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
/>
|
||||
>
|
||||
<i class="checkbox-indicator" />
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
|
@ -22,8 +22,12 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ['modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue']
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -52,7 +56,7 @@ export default {
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
vertical-align: top;
|
||||
|
@ -67,7 +71,7 @@ export default {
|
|||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: 0.5;
|
||||
opacity: .5;
|
||||
}
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
|
@ -75,7 +79,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
|
@ -88,10 +92,11 @@ export default {
|
|||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: 0.5em;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: 0.2em 8px;
|
||||
padding: .2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
|
@ -40,10 +40,9 @@
|
|||
}
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #ff00ff;
|
||||
background-color: #FF00FF;
|
||||
position: relative;
|
||||
&::before,
|
||||
&::after {
|
||||
&::before, &::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
|
@ -65,4 +64,5 @@
|
|||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,12 +14,7 @@
|
|||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@update:modelValue="
|
||||
$emit(
|
||||
'update:modelValue',
|
||||
typeof modelValue === 'undefined' ? fallback : undefined
|
||||
)
|
||||
"
|
||||
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
/>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
|
@ -29,7 +24,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
|
@ -38,7 +33,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
|
@ -46,11 +41,12 @@
|
|||
<div
|
||||
v-if="computedColor"
|
||||
class="computedIndicator"
|
||||
:style="{ backgroundColor: fallback }"
|
||||
:style="{backgroundColor: fallback}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
|
@ -97,22 +93,21 @@ export default {
|
|||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
present() {
|
||||
present () {
|
||||
return typeof this.modelValue !== 'undefined'
|
||||
},
|
||||
validColor() {
|
||||
validColor () {
|
||||
return hex2rgb(this.modelValue || this.fallback)
|
||||
},
|
||||
transparentColor() {
|
||||
transparentColor () {
|
||||
return this.modelValue === 'transparent'
|
||||
},
|
||||
computedColor() {
|
||||
computedColor () {
|
||||
return this.modelValue && this.modelValue.startsWith('--')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
|
|
|
@ -22,12 +22,13 @@ const ConfirmModal = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
onCancel () {
|
||||
this.$emit('cancelled')
|
||||
},
|
||||
onAccept() {
|
||||
onAccept () {
|
||||
this.$emit('accepted')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,6 @@
|
|||
</dialog-modal>
|
||||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../_variables';
|
||||
|
||||
|
@ -37,3 +35,5 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
|
|
@ -43,7 +43,11 @@ import {
|
|||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||
library.add(
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -61,35 +65,19 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
hint() {
|
||||
const levelVal = this.contrast.aaa
|
||||
? 'aaa'
|
||||
: this.contrast.aa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.text')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio
|
||||
})
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
},
|
||||
hint_18pt() {
|
||||
const levelVal = this.contrast.laaa
|
||||
? 'aaa'
|
||||
: this.contrast.laa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
hint_18pt () {
|
||||
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.18pt')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio
|
||||
})
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const conversationPage = {
|
|||
Conversation
|
||||
},
|
||||
computed: {
|
||||
statusId() {
|
||||
statusId () {
|
||||
return this.$route.params.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,11 @@ import {
|
|||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
|
||||
library.add(
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faChevronLeft
|
||||
)
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
|
@ -35,17 +39,16 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
|||
if (statusoid.type === 'retweet') {
|
||||
conversation = filter(
|
||||
conversation,
|
||||
(status) =>
|
||||
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
|
||||
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
||||
)
|
||||
} else {
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
}
|
||||
return conversation.filter((_) => _).sort(sortById)
|
||||
return conversation.filter(_ => _).sort(sortById)
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false,
|
||||
|
@ -63,78 +66,74 @@ const conversation = {
|
|||
'profileUserId',
|
||||
'virtualHidden'
|
||||
],
|
||||
created() {
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxDepthToShowByDefault() {
|
||||
maxDepthToShowByDefault () {
|
||||
// maxDepthInThread = max number of depths that is *visible*
|
||||
// since our depth starts with 0 and "showing" means "showing children"
|
||||
// there is a -2 here
|
||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
},
|
||||
displayStyle() {
|
||||
displayStyle () {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
},
|
||||
isTreeView() {
|
||||
isTreeView () {
|
||||
return !this.isLinearView
|
||||
},
|
||||
treeViewIsSimple() {
|
||||
treeViewIsSimple () {
|
||||
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||
},
|
||||
isLinearView() {
|
||||
isLinearView () {
|
||||
return this.displayStyle === 'linear'
|
||||
},
|
||||
shouldFadeAncestors() {
|
||||
shouldFadeAncestors () {
|
||||
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||
},
|
||||
otherRepliesButtonPosition() {
|
||||
otherRepliesButtonPosition () {
|
||||
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||
},
|
||||
showOtherRepliesButtonBelowStatus() {
|
||||
showOtherRepliesButtonBelowStatus () {
|
||||
return this.otherRepliesButtonPosition === 'below'
|
||||
},
|
||||
showOtherRepliesButtonInsideStatus() {
|
||||
showOtherRepliesButtonInsideStatus () {
|
||||
return this.otherRepliesButtonPosition === 'inside'
|
||||
},
|
||||
suspendable() {
|
||||
suspendable () {
|
||||
if (this.isTreeView) {
|
||||
return Object.entries(this.statusContentProperties).every(
|
||||
([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
|
||||
)
|
||||
return Object.entries(this.statusContentProperties)
|
||||
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||
}
|
||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
||||
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
hideStatus() {
|
||||
hideStatus () {
|
||||
return this.virtualHidden && this.suspendable
|
||||
},
|
||||
status() {
|
||||
status () {
|
||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
},
|
||||
originalStatusId() {
|
||||
originalStatusId () {
|
||||
if (this.status.retweeted_status) {
|
||||
return this.status.retweeted_status.id
|
||||
} else {
|
||||
return this.statusId
|
||||
}
|
||||
},
|
||||
conversationId() {
|
||||
conversationId () {
|
||||
return this.getConversationId(this.statusId)
|
||||
},
|
||||
conversation() {
|
||||
conversation () {
|
||||
if (!this.status) {
|
||||
return []
|
||||
}
|
||||
|
@ -143,203 +142,155 @@ const conversation = {
|
|||
return [this.status]
|
||||
}
|
||||
|
||||
const conversation = clone(
|
||||
this.$store.state.statuses.conversationsObject[this.conversationId]
|
||||
)
|
||||
const statusIndex = findIndex(conversation, {
|
||||
id: this.originalStatusId
|
||||
})
|
||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
}
|
||||
|
||||
return sortAndFilterConversation(conversation, this.status)
|
||||
},
|
||||
statusMap() {
|
||||
statusMap () {
|
||||
return this.conversation.reduce((res, s) => {
|
||||
res[s.id] = s
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
threadTree() {
|
||||
const reverseLookupTable = this.conversation.reduce(
|
||||
(table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
},
|
||||
{}
|
||||
)
|
||||
threadTree () {
|
||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
}, {})
|
||||
|
||||
const threads = this.conversation.reduce(
|
||||
(a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id).map((s) => s.id)
|
||||
const threads = this.conversation.reduce((a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id)
|
||||
.map(s => s.id)
|
||||
|
||||
return a
|
||||
},
|
||||
{
|
||||
forest: {}
|
||||
return a
|
||||
}, {
|
||||
forest: {}
|
||||
})
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
)
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||
topLevel
|
||||
.map((id) => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
processed[id] = true
|
||||
return [{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth
|
||||
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||
}).reduce((a, b) => a.concat(b), [])
|
||||
|
||||
processed[id] = true
|
||||
return [
|
||||
{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth
|
||||
},
|
||||
walk(forest, forest[id], depth + 1, processed)
|
||||
].reduce((a, b) => a.concat(b), [])
|
||||
})
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
const linearized = walk(
|
||||
threads.forest,
|
||||
this.topLevel.map((k) => k.id)
|
||||
)
|
||||
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||
|
||||
return linearized
|
||||
},
|
||||
replyIds() {
|
||||
return this.conversation
|
||||
.map((k) => k.id)
|
||||
replyIds () {
|
||||
return this.conversation.map(k => k.id)
|
||||
.reduce((res, id) => {
|
||||
res[id] = (this.replies[id] || []).map((k) => k.id)
|
||||
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyCount() {
|
||||
totalReplyCount () {
|
||||
const sizes = {}
|
||||
const subTreeSizeFor = (id) => {
|
||||
if (sizes[id]) {
|
||||
return sizes[id]
|
||||
}
|
||||
sizes[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeSizeFor(cid))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||
return sizes[id]
|
||||
}
|
||||
this.conversation.map((k) => k.id).map(subTreeSizeFor)
|
||||
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||
return Object.keys(sizes).reduce((res, id) => {
|
||||
res[id] = sizes[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyDepth() {
|
||||
totalReplyDepth () {
|
||||
const depths = {}
|
||||
const subTreeDepthFor = (id) => {
|
||||
if (depths[id]) {
|
||||
return depths[id]
|
||||
}
|
||||
depths[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeDepthFor(cid))
|
||||
.reduce((a, b) => (a > b ? a : b), 0)
|
||||
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||
return depths[id]
|
||||
}
|
||||
this.conversation.map((k) => k.id).map(subTreeDepthFor)
|
||||
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||
return Object.keys(depths).reduce((res, id) => {
|
||||
res[id] = depths[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
depths() {
|
||||
depths () {
|
||||
return this.threadTree.reduce((a, k) => {
|
||||
a[k.id] = k.depth
|
||||
return a
|
||||
}, {})
|
||||
},
|
||||
topLevel() {
|
||||
const topLevel = this.conversation.reduce(
|
||||
(tl, cur) =>
|
||||
tl.filter(
|
||||
(k) =>
|
||||
this.getReplies(cur.id)
|
||||
.map((v) => v.id)
|
||||
.indexOf(k.id) === -1
|
||||
),
|
||||
this.conversation
|
||||
)
|
||||
topLevel () {
|
||||
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||
return topLevel
|
||||
},
|
||||
otherTopLevelCount() {
|
||||
otherTopLevelCount () {
|
||||
return this.topLevel.length - 1
|
||||
},
|
||||
showingTopLevel() {
|
||||
showingTopLevel () {
|
||||
if (this.canDive && this.diveRoot) {
|
||||
return [this.statusMap[this.diveRoot]]
|
||||
}
|
||||
return this.topLevel
|
||||
},
|
||||
diveRoot() {
|
||||
diveRoot () {
|
||||
const statusId = this.inlineDivePosition || this.statusId
|
||||
const isTopLevel = !this.parentOf(statusId)
|
||||
return isTopLevel ? null : statusId
|
||||
},
|
||||
diveDepth() {
|
||||
diveDepth () {
|
||||
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||
},
|
||||
diveMode() {
|
||||
diveMode () {
|
||||
return this.canDive && !!this.diveRoot
|
||||
},
|
||||
shouldShowAllConversationButton() {
|
||||
shouldShowAllConversationButton () {
|
||||
// The "show all conversation" button tells the user that there exist
|
||||
// other toplevel statuses, so do not show it if there is only a single root
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.diveMode &&
|
||||
this.topLevel.length > 1
|
||||
)
|
||||
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||
},
|
||||
shouldShowAncestors() {
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.ancestorsOf(this.diveRoot).length
|
||||
)
|
||||
shouldShowAncestors () {
|
||||
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||
},
|
||||
replies() {
|
||||
replies () {
|
||||
let i = 1
|
||||
// eslint-disable-next-line camelcase
|
||||
return reduce(
|
||||
this.conversation,
|
||||
(result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
},
|
||||
{}
|
||||
)
|
||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
}, {})
|
||||
},
|
||||
isExpanded() {
|
||||
isExpanded () {
|
||||
return !!(this.expanded || this.isPage)
|
||||
},
|
||||
hiddenStyle() {
|
||||
hiddenStyle () {
|
||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||
return this.virtualHidden ? { height } : {}
|
||||
},
|
||||
threadDisplayStatus() {
|
||||
threadDisplayStatus () {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const depth = this.depths[id]
|
||||
|
@ -347,7 +298,7 @@ const conversation = {
|
|||
if (this.threadDisplayStatusObject[id]) {
|
||||
return this.threadDisplayStatusObject[id]
|
||||
}
|
||||
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
|
||||
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||
return 'showing'
|
||||
} else {
|
||||
return 'hidden'
|
||||
|
@ -358,7 +309,7 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
statusContentProperties() {
|
||||
statusContentProperties () {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const props = (() => {
|
||||
|
@ -383,20 +334,20 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
canDive() {
|
||||
canDive () {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
focused() {
|
||||
focused () {
|
||||
return (id) => {
|
||||
return this.isExpanded && id === this.highlight
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
}
|
||||
},
|
||||
maybeHighlight() {
|
||||
maybeHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
})
|
||||
},
|
||||
components: {
|
||||
|
@ -404,59 +355,53 @@ const conversation = {
|
|||
ThreadTree
|
||||
},
|
||||
watch: {
|
||||
statusId(newVal, oldVal) {
|
||||
statusId (newVal, oldVal) {
|
||||
const newConversationId = this.getConversationId(newVal)
|
||||
const oldConversationId = this.getConversationId(oldVal)
|
||||
if (
|
||||
newConversationId &&
|
||||
oldConversationId &&
|
||||
newConversationId === oldConversationId
|
||||
) {
|
||||
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
expanded(value) {
|
||||
expanded (value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
} else {
|
||||
this.resetDisplayState()
|
||||
}
|
||||
},
|
||||
virtualHidden(value) {
|
||||
this.$store.dispatch('setVirtualHeight', {
|
||||
statusId: this.statusId,
|
||||
height: `${this.$el.clientHeight}px`
|
||||
})
|
||||
virtualHidden (value) {
|
||||
this.$store.dispatch(
|
||||
'setVirtualHeight',
|
||||
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchConversation() {
|
||||
fetchConversation () {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchConversation({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setHighlight(this.originalStatusId)
|
||||
})
|
||||
} else {
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchStatus({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
})
|
||||
}
|
||||
},
|
||||
getReplies(id) {
|
||||
getReplies (id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
getHighlight() {
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight(id) {
|
||||
setHighlight (id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
|
||||
|
@ -467,38 +412,32 @@ const conversation = {
|
|||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
toggleExpanded() {
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
getConversationId(statusId) {
|
||||
getConversationId (statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
return get(
|
||||
status,
|
||||
'retweeted_status.statusnet_conversation_id',
|
||||
get(status, 'statusnet_conversation_id')
|
||||
)
|
||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||
},
|
||||
setThreadDisplay(id, nextStatus) {
|
||||
setThreadDisplay (id, nextStatus) {
|
||||
this.threadDisplayStatusObject = {
|
||||
...this.threadDisplayStatusObject,
|
||||
[id]: nextStatus
|
||||
}
|
||||
},
|
||||
toggleThreadDisplay(id) {
|
||||
toggleThreadDisplay (id) {
|
||||
const curStatus = this.threadDisplayStatus[id]
|
||||
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
},
|
||||
setThreadDisplayRecursively(id, nextStatus) {
|
||||
setThreadDisplayRecursively (id, nextStatus) {
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
this.getReplies(id)
|
||||
.map((k) => k.id)
|
||||
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
},
|
||||
showThreadRecursively(id) {
|
||||
showThreadRecursively (id) {
|
||||
this.setThreadDisplayRecursively(id, 'showing')
|
||||
},
|
||||
setStatusContentProperty(id, name, value) {
|
||||
setStatusContentProperty (id, name, value) {
|
||||
this.statusContentPropertiesObject = {
|
||||
...this.statusContentPropertiesObject,
|
||||
[id]: {
|
||||
|
@ -507,14 +446,10 @@ const conversation = {
|
|||
}
|
||||
}
|
||||
},
|
||||
toggleStatusContentProperty(id, name) {
|
||||
this.setStatusContentProperty(
|
||||
id,
|
||||
name,
|
||||
!this.statusContentProperties[id][name]
|
||||
)
|
||||
toggleStatusContentProperty (id, name) {
|
||||
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||
},
|
||||
leastVisibleAncestor(id) {
|
||||
leastVisibleAncestor (id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(cur)
|
||||
while (cur) {
|
||||
|
@ -528,20 +463,18 @@ const conversation = {
|
|||
// nothing found, fall back to toplevel
|
||||
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||
},
|
||||
diveIntoStatus(id, preventScroll) {
|
||||
diveIntoStatus (id, preventScroll) {
|
||||
this.tryScrollTo(id)
|
||||
},
|
||||
diveToTopLevel() {
|
||||
this.tryScrollTo(
|
||||
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
|
||||
)
|
||||
diveToTopLevel () {
|
||||
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||
},
|
||||
// only used when we are not on a page
|
||||
undive() {
|
||||
undive () {
|
||||
this.inlineDivePosition = null
|
||||
this.setHighlight(this.statusId)
|
||||
},
|
||||
tryScrollTo(id) {
|
||||
tryScrollTo (id) {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
@ -570,13 +503,13 @@ const conversation = {
|
|||
this.setHighlight(id)
|
||||
})
|
||||
},
|
||||
goToCurrent() {
|
||||
goToCurrent () {
|
||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||
},
|
||||
statusById(id) {
|
||||
statusById (id) {
|
||||
return this.statusMap[id]
|
||||
},
|
||||
parentOf(id) {
|
||||
parentOf (id) {
|
||||
const status = this.statusById(id)
|
||||
if (!status) {
|
||||
return undefined
|
||||
|
@ -587,11 +520,11 @@ const conversation = {
|
|||
}
|
||||
return parentId
|
||||
},
|
||||
parentOrSelf(id) {
|
||||
parentOrSelf (id) {
|
||||
return this.parentOf(id) || id
|
||||
},
|
||||
// Ancestors of some status, from top to bottom
|
||||
ancestorsOf(id) {
|
||||
ancestorsOf (id) {
|
||||
const ancestors = []
|
||||
let cur = this.parentOf(id)
|
||||
while (cur) {
|
||||
|
@ -600,7 +533,7 @@ const conversation = {
|
|||
}
|
||||
return ancestors
|
||||
},
|
||||
topLevelAncestorOrSelfId(id) {
|
||||
topLevelAncestorOrSelfId (id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(id)
|
||||
while (parent) {
|
||||
|
@ -609,7 +542,7 @@ const conversation = {
|
|||
}
|
||||
return cur
|
||||
},
|
||||
resetDisplayState() {
|
||||
resetDisplayState () {
|
||||
this.undive()
|
||||
this.threadDisplayStatusObject = {}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-if="!hideStatus"
|
||||
:style="hiddenStyle"
|
||||
class="Conversation"
|
||||
:class="{ '-expanded': isExpanded, panel: isExpanded }"
|
||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
|
@ -35,15 +35,13 @@
|
|||
@click.prevent="diveToTopLevel"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon icon="angle-double-left" />
|
||||
<FAIcon
|
||||
icon="angle-double-left"
|
||||
/>
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{
|
||||
$tc('status.show_all_conversation', otherTopLevelCount, {
|
||||
numStatus: otherTopLevelCount
|
||||
})
|
||||
}}
|
||||
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -56,20 +54,14 @@
|
|||
v-for="status in ancestorsOf(diveRoot)"
|
||||
:key="status.id"
|
||||
class="thread-ancestor"
|
||||
:class="{
|
||||
'thread-ancestor-has-other-replies':
|
||||
getReplies(status.id).length > 1,
|
||||
'-faded': shouldFadeAncestors
|
||||
}"
|
||||
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||
>
|
||||
<status
|
||||
ref="statusComponent"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -77,6 +69,7 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -85,47 +78,28 @@
|
|||
:total-reply-depth="totalReplyDepth"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:dive="() => diveIntoStatus(status.id)"
|
||||
:controlled-showing-tall="
|
||||
statusContentProperties[status.id].showingTall
|
||||
"
|
||||
:controlled-expanding-subject="
|
||||
statusContentProperties[status.id].expandingSubject
|
||||
"
|
||||
:controlled-showing-long-subject="
|
||||
statusContentProperties[status.id].showingLongSubject
|
||||
"
|
||||
|
||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||
:controlled-replying="statusContentProperties[status.id].replying"
|
||||
:controlled-media-playing="
|
||||
statusContentProperties[status.id].mediaPlaying
|
||||
"
|
||||
:controlled-toggle-showing-tall="
|
||||
() => toggleStatusContentProperty(status.id, 'showingTall')
|
||||
"
|
||||
:controlled-toggle-expanding-subject="
|
||||
() => toggleStatusContentProperty(status.id, 'expandingSubject')
|
||||
"
|
||||
:controlled-toggle-showing-long-subject="
|
||||
() =>
|
||||
toggleStatusContentProperty(status.id, 'showingLongSubject')
|
||||
"
|
||||
:controlled-toggle-replying="
|
||||
() => toggleStatusContentProperty(status.id, 'replying')
|
||||
"
|
||||
:controlled-set-media-playing="
|
||||
(newVal) =>
|
||||
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
|
||||
"
|
||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
showOtherRepliesButtonBelowStatus &&
|
||||
getReplies(status.id).length > 1
|
||||
"
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
class="thread-ancestor-dive-box"
|
||||
>
|
||||
<div class="thread-ancestor-dive-box-inner">
|
||||
<div
|
||||
class="thread-ancestor-dive-box-inner"
|
||||
>
|
||||
<i18n-t
|
||||
tag="button"
|
||||
scope="global"
|
||||
|
@ -134,17 +108,13 @@
|
|||
@click.prevent="diveIntoStatus(status.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon icon="angle-double-right" />
|
||||
<FAIcon
|
||||
icon="angle-double-right"
|
||||
/>
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{
|
||||
$tc(
|
||||
'status.ancestor_follow',
|
||||
getReplies(status.id).length - 1,
|
||||
{ numReplies: getReplies(status.id).length - 1 }
|
||||
)
|
||||
}}
|
||||
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -157,6 +127,7 @@
|
|||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:depth="0"
|
||||
|
||||
:status="status"
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
|
@ -164,11 +135,13 @@
|
|||
:is-expanded="isExpanded"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:focused="focused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -192,9 +165,7 @@
|
|||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -202,6 +173,7 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
|
@ -210,6 +182,7 @@
|
|||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
|
@ -260,8 +233,7 @@
|
|||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
/* Make the button stretch along the whole row */
|
||||
&,
|
||||
&-inner {
|
||||
&, &-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
@ -281,7 +253,8 @@
|
|||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
.thread-ancestor:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||
&:last-child .conversation-status {
|
||||
&:last-child .conversation-status,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
@ -298,8 +271,7 @@
|
|||
border-left-color: $fallback--cRed;
|
||||
border-left-color: var(--cRed, $fallback--cRed);
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
|
||||
var(--panelRadius, $fallback--panelRadius);
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
publicTimelineVisible,
|
||||
federatedTimelineVisible,
|
||||
bubbleTimelineVisible
|
||||
bubbleTimelineVisible,
|
||||
} from '../../lib/timeline_visibility'
|
||||
import {
|
||||
faSignInAlt,
|
||||
|
@ -52,103 +52,81 @@ export default {
|
|||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
supportsMask:
|
||||
window.CSS &&
|
||||
window.CSS.supports &&
|
||||
(window.CSS.supports('mask-size', 'contain') ||
|
||||
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')),
|
||||
window.CSS.supports('-o-mask-size', 'contain')
|
||||
),
|
||||
showingConfirmLogout: false
|
||||
}),
|
||||
computed: {
|
||||
enableMask() {
|
||||
return this.supportsMask && this.$store.state.instance.logoMask
|
||||
},
|
||||
logoStyle() {
|
||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||
logoStyle () {
|
||||
return {
|
||||
visibility: this.enableMask ? 'hidden' : 'visible'
|
||||
'visibility': this.enableMask ? 'hidden' : 'visible'
|
||||
}
|
||||
},
|
||||
logoMaskStyle() {
|
||||
return this.enableMask
|
||||
? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
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.searchBarHidden ? 1 : 0
|
||||
},
|
||||
this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
)
|
||||
logoBgStyle () {
|
||||
return Object.assign({
|
||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
}, this.enableMask ? {} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
},
|
||||
logo() {
|
||||
return this.$store.state.instance.logo
|
||||
},
|
||||
mergedConfig() {
|
||||
logo () { return this.$store.state.instance.logo },
|
||||
mergedConfig () {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
sitename() {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
showNavShortcuts() {
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
showNavShortcuts () {
|
||||
return this.mergedConfig.showNavShortcuts
|
||||
},
|
||||
showWiderShortcuts() {
|
||||
showWiderShortcuts () {
|
||||
return this.mergedConfig.showWiderShortcuts
|
||||
},
|
||||
hideSiteFavicon() {
|
||||
hideSiteFavicon () {
|
||||
return this.mergedConfig.hideSiteFavicon
|
||||
},
|
||||
hideSiteName() {
|
||||
hideSiteName () {
|
||||
return this.mergedConfig.hideSiteName
|
||||
},
|
||||
hideSitename() {
|
||||
return this.$store.state.instance.hideSitename
|
||||
},
|
||||
logoLeft() {
|
||||
return this.$store.state.instance.logoLeft
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
shouldConfirmLogout() {
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
showBubbleTimeline() {
|
||||
showBubbleTimeline () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
},
|
||||
...mapState({
|
||||
publicTimelineVisible,
|
||||
federatedTimelineVisible,
|
||||
bubbleTimelineVisible
|
||||
bubbleTimelineVisible,
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
scrollToTop () {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
onSearchBarToggled(hidden) {
|
||||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal() {
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal() {
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
display: grid;
|
||||
grid-template-rows: var(--navbar-height);
|
||||
grid-template-columns: 2fr auto 2fr;
|
||||
grid-template-areas: 'nav-left logo actions';
|
||||
grid-template-areas: "nav-left logo actions";
|
||||
box-sizing: border-box;
|
||||
padding: 0 1.2em;
|
||||
margin: auto;
|
||||
|
@ -24,12 +24,11 @@
|
|||
|
||||
&.-logoLeft .inner-nav {
|
||||
grid-template-columns: auto 2fr 2fr;
|
||||
grid-template-areas: 'logo nav-left actions';
|
||||
grid-template-areas: "logo nav-left actions";
|
||||
}
|
||||
|
||||
.button-default {
|
||||
&,
|
||||
svg {
|
||||
&, svg {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
@ -50,7 +49,7 @@
|
|||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,25 +88,13 @@
|
|||
width: 2em;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
&.router-link-active {
|
||||
// box-shadow: 0 -2px 0 var(--selectedMenuText, $fallback--text) inset;
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
}
|
||||
font-size: 1.2em;
|
||||
margin-top: 0.05em;
|
||||
|
||||
.svg-inline--fa {
|
||||
font-weight: bolder;
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
<img
|
||||
v-if="!hideSiteFavicon"
|
||||
class="favicon"
|
||||
src="/favicon.svg"
|
||||
/>
|
||||
src="/favicon.png"
|
||||
>
|
||||
<span
|
||||
v-if="!hideSiteName"
|
||||
class="site-name"
|
||||
|
@ -44,9 +44,9 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="publicTimelineVisible"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="publicTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -68,9 +68,9 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federatedTimelineVisible"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="federatedTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -93,7 +93,7 @@
|
|||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
/>
|
||||
>
|
||||
</router-link>
|
||||
<div class="item right actions">
|
||||
<search-bar
|
||||
|
@ -108,10 +108,7 @@
|
|||
<router-link
|
||||
v-if="currentUser"
|
||||
class="nav-icon"
|
||||
:to="{
|
||||
name: 'interactions',
|
||||
params: { username: currentUser.screen_name }
|
||||
}"
|
||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -157,10 +154,7 @@
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
(currentUser && currentUser.role === 'admin') ||
|
||||
currentUser.role === 'moderator'
|
||||
"
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openModModal"
|
||||
>
|
||||
|
|
|
@ -31,14 +31,14 @@
|
|||
.dark-overlay {
|
||||
&::before {
|
||||
bottom: 0;
|
||||
content: ' ';
|
||||
content: " ";
|
||||
display: block;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgba(27, 31, 35, 0.5);
|
||||
background: rgba(27,31,35,.5);
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em;
|
||||
padding: .5em .5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
|
@ -84,8 +84,9 @@
|
|||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: 0.5rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const DMs = {
|
||||
computed: {
|
||||
timeline() {
|
||||
timeline () {
|
||||
return this.$store.state.statuses.timelines.dms
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,18 +6,18 @@ const DomainMuteCard = {
|
|||
ProgressButton
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
muted() {
|
||||
muted () {
|
||||
return this.user.domainMutes.includes(this.domain)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain() {
|
||||
unmuteDomain () {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
},
|
||||
muteDomain() {
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template #progress>
|
||||
<template v-slot:progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
@ -19,7 +19,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template #progress>
|
||||
<template v-slot:progress>
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
|
|
@ -8,27 +8,27 @@ const EditStatusModal = {
|
|||
PostStatusForm,
|
||||
Modal
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
resettingForm: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn() {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated() {
|
||||
modalActivated () {
|
||||
return this.$store.state.editStatus.modalActivated
|
||||
},
|
||||
isFormVisible() {
|
||||
isFormVisible () {
|
||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||
},
|
||||
params() {
|
||||
params () {
|
||||
return this.$store.state.editStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params(newVal, oldVal) {
|
||||
params (newVal, oldVal) {
|
||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
|
@ -36,16 +36,14 @@ const EditStatusModal = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFormVisible(val) {
|
||||
isFormVisible (val) {
|
||||
if (val) {
|
||||
this.$nextTick(
|
||||
() => this.$el && this.$el.querySelector('textarea').focus()
|
||||
)
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
const params = {
|
||||
store: this.$store,
|
||||
statusId: this.$store.state.editStatus.params.statusId,
|
||||
|
@ -57,8 +55,7 @@ const EditStatusModal = {
|
|||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService
|
||||
.editStatus(params)
|
||||
return statusPosterService.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
@ -69,7 +66,7 @@ const EditStatusModal = {
|
|||
}
|
||||
})
|
||||
},
|
||||
closeModal() {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
:post-handler="doEditStatus"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:post-handler="doEditStatus"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -9,7 +9,7 @@ const EmojiGrid = {
|
|||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
containerWidth: 0,
|
||||
containerHeight: 0,
|
||||
|
@ -17,7 +17,7 @@ const EmojiGrid = {
|
|||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
this.containerWidth = rect.width
|
||||
this.containerHeight = rect.height
|
||||
|
@ -29,29 +29,29 @@ const EmojiGrid = {
|
|||
})
|
||||
this.resizeObserver.observe(this.$refs.container)
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeUnmount () {
|
||||
this.resizeObserver.disconnect()
|
||||
this.resizeObserver = null
|
||||
},
|
||||
watch: {
|
||||
groups() {
|
||||
groups () {
|
||||
// Scroll to top when grid content changes
|
||||
if (this.$refs.container) {
|
||||
this.$refs.container.scrollTo(0, 0)
|
||||
}
|
||||
},
|
||||
activeGroup(group) {
|
||||
activeGroup (group) {
|
||||
this.$emit('activeGroup', group)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
onScroll () {
|
||||
this.scrollPos = this.$refs.container.scrollTop
|
||||
},
|
||||
onEmoji(emoji) {
|
||||
onEmoji (emoji) {
|
||||
this.$emit('emoji', emoji)
|
||||
},
|
||||
scrollToItem(itemId) {
|
||||
scrollToItem (itemId) {
|
||||
const container = this.$refs.container
|
||||
if (!container) return
|
||||
|
||||
|
@ -65,7 +65,7 @@ const EmojiGrid = {
|
|||
},
|
||||
computed: {
|
||||
// Total height of scroller content
|
||||
gridHeight() {
|
||||
gridHeight () {
|
||||
if (this.itemList.length === 0) return 0
|
||||
const lastItem = this.itemList[this.itemList.length - 1]
|
||||
return (
|
||||
|
@ -73,7 +73,7 @@ const EmojiGrid = {
|
|||
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
|
||||
)
|
||||
},
|
||||
activeGroup() {
|
||||
activeGroup () {
|
||||
const items = this.itemList
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const item = items[i]
|
||||
|
@ -83,7 +83,7 @@ const EmojiGrid = {
|
|||
}
|
||||
return null
|
||||
},
|
||||
itemList() {
|
||||
itemList () {
|
||||
const items = []
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
@ -111,14 +111,14 @@ const EmojiGrid = {
|
|||
}
|
||||
return items
|
||||
},
|
||||
visibleItems() {
|
||||
visibleItems () {
|
||||
const startPos = this.scrollPos - BUFFER_SIZE
|
||||
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
|
||||
return this.itemList.filter((i) => {
|
||||
return i.position.y >= startPos && i.position.y < endPos
|
||||
})
|
||||
},
|
||||
scrolledClass() {
|
||||
scrolledClass () {
|
||||
if (this.scrollPos <= 5) {
|
||||
return 'scrolled-top'
|
||||
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
>
|
||||
<div
|
||||
:style="{
|
||||
height: `${gridHeight}px`
|
||||
height: `${gridHeight}px`,
|
||||
}"
|
||||
>
|
||||
<template v-for="item in visibleItems">
|
||||
|
@ -37,7 +37,7 @@
|
|||
<img
|
||||
v-else
|
||||
:src="item.emoji.imageUrl"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -4,9 +4,13 @@ import { take } from 'lodash'
|
|||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||
import {
|
||||
faSmileBeam
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(faSmileBeam)
|
||||
library.add(
|
||||
faSmileBeam
|
||||
)
|
||||
|
||||
/**
|
||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||
|
@ -101,7 +105,7 @@ const EmojiInput = {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
input: undefined,
|
||||
highlighted: 0,
|
||||
|
@ -119,34 +123,29 @@ const EmojiInput = {
|
|||
EmojiPicker
|
||||
},
|
||||
computed: {
|
||||
padEmoji() {
|
||||
padEmoji () {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
showSuggestions() {
|
||||
return (
|
||||
this.focused &&
|
||||
showSuggestions () {
|
||||
return this.focused &&
|
||||
this.suggestions &&
|
||||
this.suggestions.length > 0 &&
|
||||
!this.showPicker &&
|
||||
!this.temporarilyHideSuggestions
|
||||
)
|
||||
},
|
||||
textAtCaret() {
|
||||
textAtCaret () {
|
||||
return (this.wordAtCaret || {}).word || ''
|
||||
},
|
||||
wordAtCaret() {
|
||||
wordAtCaret () {
|
||||
if (this.modelValue && this.caret) {
|
||||
const word =
|
||||
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
return word
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
const { root } = this.$refs
|
||||
const input =
|
||||
root.querySelector('.emoji-input > input') ||
|
||||
root.querySelector('.emoji-input > textarea')
|
||||
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||
if (!input) return
|
||||
this.input = input
|
||||
this.resize()
|
||||
|
@ -159,7 +158,7 @@ const EmojiInput = {
|
|||
input.addEventListener('transitionend', this.onTransition)
|
||||
input.addEventListener('input', this.onInput)
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
const { input } = this
|
||||
if (input) {
|
||||
input.removeEventListener('blur', this.onBlur)
|
||||
|
@ -184,28 +183,27 @@ const EmojiInput = {
|
|||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord) return
|
||||
if (matchedSuggestions.length <= 0) return
|
||||
this.suggestions = take(matchedSuggestions, 5).map(
|
||||
({ imageUrl, ...rest }) => ({
|
||||
this.suggestions = take(matchedSuggestions, 5)
|
||||
.map(({ imageUrl, ...rest }) => ({
|
||||
...rest,
|
||||
img: imageUrl || ''
|
||||
})
|
||||
)
|
||||
}))
|
||||
},
|
||||
suggestions: {
|
||||
handler(newValue) {
|
||||
handler (newValue) {
|
||||
this.$nextTick(this.resize)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focusPickerInput() {
|
||||
focusPickerInput () {
|
||||
const pickerEl = this.$refs.picker.$el
|
||||
if (!pickerEl) return
|
||||
const pickerInput = pickerEl.querySelector('input')
|
||||
if (pickerInput) pickerInput.focus()
|
||||
},
|
||||
triggerShowPicker() {
|
||||
triggerShowPicker () {
|
||||
this.showPicker = true
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
|
@ -219,7 +217,7 @@ const EmojiInput = {
|
|||
this.disableClickOutside = false
|
||||
}, 0)
|
||||
},
|
||||
togglePicker() {
|
||||
togglePicker () {
|
||||
this.input.focus()
|
||||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
|
@ -227,16 +225,12 @@ const EmojiInput = {
|
|||
this.$nextTick(this.focusPickerInput)
|
||||
}
|
||||
},
|
||||
replace(replacement) {
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement
|
||||
)
|
||||
replace (replacement) {
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
insert({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
const before = this.modelValue.substring(0, this.caret) || ''
|
||||
const after = this.modelValue.substring(this.caret) || ''
|
||||
|
||||
|
@ -255,25 +249,19 @@ const EmojiInput = {
|
|||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||
*/
|
||||
const isSpaceRegex = /\s/
|
||||
const spaceBefore =
|
||||
surroundingSpace &&
|
||||
!isSpaceRegex.exec(before.slice(-1)) &&
|
||||
before.length &&
|
||||
this.padEmoji > 0
|
||||
? ' '
|
||||
: ''
|
||||
const spaceAfter =
|
||||
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
|
||||
? ' '
|
||||
: ''
|
||||
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
||||
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
||||
|
||||
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
|
||||
''
|
||||
)
|
||||
const newValue = [
|
||||
before,
|
||||
spaceBefore,
|
||||
insertion,
|
||||
spaceAfter,
|
||||
after
|
||||
].join('')
|
||||
this.keepOpen = keepOpen
|
||||
this.$emit('update:modelValue', newValue)
|
||||
const position =
|
||||
this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
if (!keepOpen) {
|
||||
this.input.focus()
|
||||
}
|
||||
|
@ -285,17 +273,12 @@ const EmojiInput = {
|
|||
this.caret = position
|
||||
})
|
||||
},
|
||||
replaceText(e, suggestion) {
|
||||
replaceText (e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion =
|
||||
suggestion || this.suggestions[this.highlighted]
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement
|
||||
)
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.highlighted = 0
|
||||
const position = this.wordAtCaret.start + replacement.length
|
||||
|
@ -310,7 +293,7 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
cycleBackward(e) {
|
||||
cycleBackward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted -= 1
|
||||
|
@ -322,7 +305,7 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleForward(e) {
|
||||
cycleForward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted += 1
|
||||
|
@ -334,28 +317,26 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
scrollIntoView() {
|
||||
scrollIntoView () {
|
||||
const rootRef = this.$refs['picker'].$el
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const scrollerRef =
|
||||
this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll =
|
||||
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
|
||||
const scrollerHeight =
|
||||
scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll = scrollerRef === window
|
||||
? scrollerRef.scrollY
|
||||
: scrollerRef.scrollTop
|
||||
const scrollerHeight = scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
|
||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||
// We check where the bottom border of root element is, this uses findOffset
|
||||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder =
|
||||
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
|
||||
// could also check top delta but there's no case for it
|
||||
|
@ -377,10 +358,10 @@ const EmojiInput = {
|
|||
}
|
||||
})
|
||||
},
|
||||
onTransition(e) {
|
||||
onTransition (e) {
|
||||
this.resize()
|
||||
},
|
||||
onBlur(e) {
|
||||
onBlur (e) {
|
||||
// Clicking on any suggestion removes focus from autocomplete,
|
||||
// preventing click handler ever executing.
|
||||
this.blurTimeout = setTimeout(() => {
|
||||
|
@ -389,10 +370,10 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
}, 200)
|
||||
},
|
||||
onClick(e, suggestion) {
|
||||
onClick (e, suggestion) {
|
||||
this.replaceText(e, suggestion)
|
||||
},
|
||||
onFocus(e) {
|
||||
onFocus (e) {
|
||||
if (this.blurTimeout) {
|
||||
clearTimeout(this.blurTimeout)
|
||||
this.blurTimeout = null
|
||||
|
@ -406,7 +387,7 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.temporarilyHideSuggestions = false
|
||||
},
|
||||
onKeyUp(e) {
|
||||
onKeyUp (e) {
|
||||
const { key } = e
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
|
@ -419,11 +400,11 @@ const EmojiInput = {
|
|||
this.temporarilyHideSuggestions = false
|
||||
}
|
||||
},
|
||||
onPaste(e) {
|
||||
onPaste (e) {
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
},
|
||||
onKeyDown(e) {
|
||||
onKeyDown (e) {
|
||||
const { ctrlKey, shiftKey, key } = e
|
||||
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||
|
@ -470,31 +451,31 @@ const EmojiInput = {
|
|||
this.showPicker = false
|
||||
this.resize()
|
||||
},
|
||||
onInput(e) {
|
||||
onInput (e) {
|
||||
this.showPicker = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.$emit('update:modelValue', e.target.value)
|
||||
},
|
||||
onClickInput(e) {
|
||||
onClickInput (e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
onClickOutside(e) {
|
||||
onClickOutside (e) {
|
||||
if (this.disableClickOutside) return
|
||||
this.showPicker = false
|
||||
},
|
||||
onStickerUploaded(e) {
|
||||
onStickerUploaded (e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed(e) {
|
||||
onStickerUploadFailed (e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-upload-Failed', e)
|
||||
},
|
||||
setCaret({ target: { selectionStart } }) {
|
||||
setCaret ({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
},
|
||||
resize() {
|
||||
resize () {
|
||||
const panel = this.$refs.panel
|
||||
if (!panel) return
|
||||
const picker = this.$refs.picker.$el
|
||||
|
@ -505,12 +486,9 @@ const EmojiInput = {
|
|||
this.setPlacement(panelBody, panel, offsetBottom)
|
||||
this.setPlacement(picker, picker, offsetBottom)
|
||||
},
|
||||
setPlacement(container, target, offsetBottom) {
|
||||
setPlacement (container, target, offsetBottom) {
|
||||
if (!container || !target) return
|
||||
if (
|
||||
this.placement === 'bottom' ||
|
||||
(this.placement === 'auto' && !this.overflowsBottom(container))
|
||||
) {
|
||||
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) {
|
||||
target.style.top = offsetBottom + 'px'
|
||||
target.style.bottom = 'auto'
|
||||
} else {
|
||||
|
@ -518,7 +496,7 @@ const EmojiInput = {
|
|||
target.style.bottom = this.input.offsetHeight + 'px'
|
||||
}
|
||||
},
|
||||
overflowsBottom(el) {
|
||||
overflowsBottom (el) {
|
||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,14 +43,11 @@
|
|||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span
|
||||
v-if="!suggestion.mfm"
|
||||
class="image"
|
||||
>
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
/>
|
||||
>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
|
@ -81,7 +78,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0.2em 0.25em;
|
||||
margin: .2em .25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
@ -97,7 +94,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +105,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
display: none
|
||||
}
|
||||
|
||||
&-body {
|
||||
|
@ -182,8 +179,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
input, textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
const MFM_TAGS = [
|
||||
'blur',
|
||||
'bounce',
|
||||
'flip',
|
||||
'font',
|
||||
'jelly',
|
||||
'jump',
|
||||
'rainbow',
|
||||
'rotate',
|
||||
'shake',
|
||||
'sparkle',
|
||||
'spin',
|
||||
'tada',
|
||||
'twitch',
|
||||
'x2',
|
||||
'x3',
|
||||
'x4'
|
||||
].map((tag) => ({
|
||||
displayText: tag,
|
||||
detailText: '$[' + tag + ' ]',
|
||||
replacement: '$[' + tag + ' ]',
|
||||
mfm: true
|
||||
}))
|
||||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
|
@ -34,10 +13,10 @@ const MFM_TAGS = [
|
|||
* doesn't support user linking you can just provide only emoji.
|
||||
*/
|
||||
|
||||
export default (data) => {
|
||||
export default data => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return (input) => {
|
||||
return input => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return emojiCurry(input)
|
||||
|
@ -46,15 +25,14 @@ export default (data) => {
|
|||
return usersCurry(input)
|
||||
}
|
||||
if (firstChar === '$') {
|
||||
return MFM_TAGS.filter(
|
||||
({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
|
||||
)
|
||||
return MFM_TAGS
|
||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const suggestEmoji = (emojis) => (input) => {
|
||||
export const suggestEmoji = emojis => input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
|
@ -107,7 +85,7 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return async (input) => {
|
||||
return async input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
if (previousQuery === noPrefix) return suggestions
|
||||
|
||||
|
@ -121,47 +99,36 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
await debounceUserSearch(noPrefix)
|
||||
}
|
||||
|
||||
const newSuggestions = state.users.users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
)
|
||||
.slice(0, 20)
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
|
||||
const diff = (bScore - aScore) * 10
|
||||
const diff = (bScore - aScore) * 10
|
||||
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
})
|
||||
.map(
|
||||
({
|
||||
screen_name,
|
||||
screen_name_ui,
|
||||
name,
|
||||
profile_image_url_original
|
||||
}) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
})
|
||||
)
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
suggestions = newSuggestions || []
|
||||
|
|
|
@ -7,9 +7,13 @@ import {
|
|||
faStickyNote,
|
||||
faSmileBeam
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { trim, escapeRegExp, startCase } from 'lodash'
|
||||
import { trim, escapeRegExp, startCase, debounce } from 'lodash'
|
||||
|
||||
library.add(faBoxOpen, faStickyNote, faSmileBeam)
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
)
|
||||
|
||||
const EmojiPicker = {
|
||||
props: {
|
||||
|
@ -24,7 +28,7 @@ const EmojiPicker = {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
|
@ -33,71 +37,58 @@ const EmojiPicker = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() =>
|
||||
import('../sticker_picker/sticker_picker.vue')
|
||||
),
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox,
|
||||
EmojiGrid
|
||||
},
|
||||
methods: {
|
||||
onStickerUploaded(e) {
|
||||
debouncedSearch: debounce(function (e) {
|
||||
this.keyword = e.target.value
|
||||
}, 500),
|
||||
onStickerUploaded (e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed(e) {
|
||||
onStickerUploadFailed (e) {
|
||||
this.$emit('sticker-upload-failed', e)
|
||||
},
|
||||
onEmoji(emoji) {
|
||||
const value = emoji.imageUrl
|
||||
? `:${emoji.displayText}:`
|
||||
: emoji.replacement
|
||||
onEmoji (emoji) {
|
||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||
this.$store.commit('emojiUsed', emoji)
|
||||
},
|
||||
onWheel(e) {
|
||||
onWheel (e) {
|
||||
e.preventDefault()
|
||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||
},
|
||||
highlight(key) {
|
||||
highlight (key) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
if (this.keyword.length) {
|
||||
this.$refs.emojiGrid.scrollToItem(key)
|
||||
}
|
||||
},
|
||||
onActiveGroup(group) {
|
||||
onActiveGroup (group) {
|
||||
this.activeGroup = group
|
||||
},
|
||||
toggleStickers() {
|
||||
toggleStickers () {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
setShowStickers(value) {
|
||||
setShowStickers (value) {
|
||||
this.showingStickers = value
|
||||
},
|
||||
filterByKeyword(list) {
|
||||
filterByKeyword (list) {
|
||||
if (this.keyword === '') return list
|
||||
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
||||
return list.filter((emoji) => {
|
||||
return (
|
||||
regex.test(emoji.displayText) ||
|
||||
(!emoji.imageUrl && emoji.replacement === this.keyword)
|
||||
)
|
||||
return list.filter(emoji => {
|
||||
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeGroupView() {
|
||||
activeGroupView () {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
},
|
||||
stickersAvailable() {
|
||||
if (this.$store.state.instance.stickers) {
|
||||
return this.$store.state.instance.stickers.length > 0
|
||||
}
|
||||
return 0
|
||||
},
|
||||
filteredEmoji() {
|
||||
return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
|
||||
},
|
||||
emojis() {
|
||||
emojis () {
|
||||
const recentEmojis = this.$store.getters.recentEmojis
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.sortedEmoji
|
||||
|
@ -116,7 +107,7 @@ const EmojiPicker = {
|
|||
text: this.$t('emoji.recent'),
|
||||
first: {
|
||||
imageUrl: '',
|
||||
replacement: '🕒'
|
||||
replacement: '🕒',
|
||||
},
|
||||
emojis: this.filterByKeyword(recentEmojis)
|
||||
},
|
||||
|
@ -131,7 +122,7 @@ const EmojiPicker = {
|
|||
}
|
||||
].concat(emojiPacks)
|
||||
},
|
||||
sortedEmoji() {
|
||||
sortedEmoji () {
|
||||
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||
const sortedEmojiGroups = new Map()
|
||||
customEmojis.forEach((emoji) => {
|
||||
|
@ -143,22 +134,19 @@ const EmojiPicker = {
|
|||
})
|
||||
return new Map([...sortedEmojiGroups.entries()].sort())
|
||||
},
|
||||
emojisView() {
|
||||
emojisView () {
|
||||
if (this.keyword === '') {
|
||||
return this.emojis.filter((pack) => {
|
||||
return this.emojis.filter(pack => {
|
||||
return pack.id === this.activeGroup
|
||||
})
|
||||
} else {
|
||||
return this.emojis.filter((pack) => {
|
||||
return this.emojis.filter(pack => {
|
||||
return pack.emojis.length > 0
|
||||
})
|
||||
}
|
||||
},
|
||||
stickerPickerEnabled() {
|
||||
return (
|
||||
(this.$store.state.instance.stickers || []).length !== 0 &&
|
||||
this.enableStickerPicker
|
||||
)
|
||||
stickerPickerEnabled () {
|
||||
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,7 @@
|
|||
// The worst query selector ever
|
||||
// selects ONLY emojis pickers in replies in notifications
|
||||
// who thought this was a good idea?
|
||||
.notification
|
||||
> .Status
|
||||
> .status-container
|
||||
> .post-status-form
|
||||
> form
|
||||
> .form-group
|
||||
> .emoji-input
|
||||
> .emoji-picker {
|
||||
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
|
||||
max-width: 100%;
|
||||
left: 0;
|
||||
@media (min-width: 1300px) {
|
||||
|
@ -118,7 +111,7 @@
|
|||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0.4em;
|
||||
padding: .4em;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
|
@ -151,7 +144,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 auto
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
@ -177,65 +170,5 @@
|
|||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-groups {
|
||||
flex: 1 1 1px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
&.scrolled {
|
||||
&-top {
|
||||
mask-size: 100% 20px, 100% 0, auto;
|
||||
}
|
||||
&-bottom {
|
||||
mask-size: 100% 0, 100% 20px, auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding-left: 5px;
|
||||
justify-content: left;
|
||||
|
||||
&-title {
|
||||
font-size: 0.85em;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
&.disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,18 +17,16 @@
|
|||
:title="group.text"
|
||||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<span v-if="!group.first.imageUrl">{{
|
||||
group.first.replacement
|
||||
}}</span>
|
||||
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
|
||||
<img
|
||||
v-else
|
||||
:src="group.first.imageUrl"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
class="stickers-tab-icon emoji-tabs-item"
|
||||
:class="{ active: showingStickers }"
|
||||
:class="{active: showingStickers}"
|
||||
:title="$t('emoji.stickers')"
|
||||
@click.prevent="toggleStickers"
|
||||
>
|
||||
|
@ -42,16 +40,15 @@
|
|||
<div class="content">
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{ hidden: showingStickers }"
|
||||
:class="{hidden: showingStickers}"
|
||||
>
|
||||
<div class="emoji-search">
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
/>
|
||||
@input="debouncedSearch"
|
||||
>
|
||||
</div>
|
||||
<EmojiGrid
|
||||
ref="emojiGrid"
|
||||
|
@ -63,34 +60,6 @@
|
|||
v-if="showKeepOpen"
|
||||
class="keep-open"
|
||||
>
|
||||
<div
|
||||
v-for="group in emojisView"
|
||||
:key="group.id"
|
||||
class="emoji-group"
|
||||
>
|
||||
<h6
|
||||
:ref="'group-' + group.id"
|
||||
class="emoji-group-title"
|
||||
>
|
||||
{{ group.text }}
|
||||
</h6>
|
||||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="emoji.displayText"
|
||||
class="emoji-item"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
>
|
||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
||||
<img
|
||||
v-else
|
||||
:src="emoji.imageUrl"
|
||||
/>
|
||||
</span>
|
||||
<span :ref="'group-end-' + group.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="keep-open">
|
||||
<Checkbox v-model="keepOpen">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
</Checkbox>
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
const findEmojiByReplacement = (state, replacement) => {
|
||||
const allEmojis = state.instance.emoji.concat(state.instance.customEmoji)
|
||||
return allEmojis.find((emoji) => emoji.replacement === replacement)
|
||||
return allEmojis.find(emoji => emoji.replacement === replacement)
|
||||
}
|
||||
|
||||
const EmojiReactions = {
|
||||
name: 'EmojiReactions',
|
||||
components: {
|
||||
UserAvatar,
|
||||
UserListPopover
|
||||
UserListPopover,
|
||||
StillImage
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
showAll: false
|
||||
}),
|
||||
computed: {
|
||||
tooManyReactions() {
|
||||
tooManyReactions () {
|
||||
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||
},
|
||||
emojiReactions() {
|
||||
emojiReactions () {
|
||||
return this.showAll
|
||||
? this.status.emoji_reactions
|
||||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||
},
|
||||
showMoreString() {
|
||||
return `+${
|
||||
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
|
||||
}`
|
||||
showMoreString () {
|
||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||
},
|
||||
accountsForEmoji() {
|
||||
accountsForEmoji () {
|
||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||
if (reaction.url) {
|
||||
acc[reaction.url] = reaction.accounts || []
|
||||
|
@ -42,32 +42,32 @@ const EmojiReactions = {
|
|||
return acc
|
||||
}, {})
|
||||
},
|
||||
loggedIn() {
|
||||
loggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleShowAll() {
|
||||
toggleShowAll () {
|
||||
this.showAll = !this.showAll
|
||||
},
|
||||
reactedWith(emoji) {
|
||||
return this.status.emoji_reactions.find((r) => r.name === emoji).me
|
||||
reactedWith (emoji) {
|
||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||
},
|
||||
fetchEmojiReactionsByIfMissing() {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
|
||||
fetchEmojiReactionsByIfMissing () {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||
if (hasNoAccounts) {
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
}
|
||||
},
|
||||
reactWith(emoji) {
|
||||
reactWith (emoji) {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
|
||||
this.$store.commit('emojiUsed', emojiObject)
|
||||
},
|
||||
unreact(emoji) {
|
||||
unreact (emoji) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
emojiOnClick(emoji, event) {
|
||||
emojiOnClick (emoji, event) {
|
||||
if (!this.loggedIn) return
|
||||
|
||||
if (this.reactedWith(emoji)) {
|
||||
|
|
|
@ -1,35 +1,29 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<UserListPopover
|
||||
v-for="reaction in emojiReactions"
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.url || reaction.name"
|
||||
:users="accountsForEmoji[reaction.url || reaction.name]"
|
||||
>
|
||||
<button
|
||||
class="emoji-reaction btn button-default"
|
||||
:class="{
|
||||
'picked-reaction': reactedWith(reaction.name),
|
||||
'not-clickable': !loggedIn
|
||||
}"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span
|
||||
v-if="reaction.url !== null"
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<img
|
||||
<StillImage
|
||||
:src="reaction.url"
|
||||
:title="reaction.name"
|
||||
:alt="reaction.name"
|
||||
class="reaction-emoji"
|
||||
width="2.55em"
|
||||
height="2.55em"
|
||||
/>
|
||||
{{ reaction.count }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<span v-else>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
|
@ -48,7 +42,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
|
@ -60,7 +54,7 @@
|
|||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 128%;
|
||||
font-size: 210%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
|
@ -68,7 +62,6 @@
|
|||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
@ -78,10 +71,6 @@
|
|||
height: 2.55em !important;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
img.reaction-emoji {
|
||||
width: 1.55em !important;
|
||||
display: block;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
@ -108,12 +97,9 @@
|
|||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
background: none;
|
||||
padding: 1px 0.5em;
|
||||
|
||||
.emoji-button-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCircleNotch)
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
|
||||
const Exporter = {
|
||||
props: {
|
||||
|
@ -16,30 +18,26 @@ const Exporter = {
|
|||
exportButtonLabel: { type: String },
|
||||
processingMessage: { type: String }
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
process() {
|
||||
process () {
|
||||
this.processing = true
|
||||
this.getContent().then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
|
||||
)
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => {
|
||||
this.processing = false
|
||||
}, 2000)
|
||||
})
|
||||
this.getContent()
|
||||
.then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => { this.processing = false }, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ const ExtraButtons = {
|
|||
Popover,
|
||||
ConfirmModal
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
showingDeleteDialog: false,
|
||||
|
@ -44,134 +44,105 @@ const ExtraButtons = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
deleteStatus() {
|
||||
deleteStatus () {
|
||||
if (this.shouldConfirmDelete) {
|
||||
this.showDeleteStatusConfirmDialog()
|
||||
} else {
|
||||
this.doDeleteStatus()
|
||||
}
|
||||
},
|
||||
doDeleteStatus() {
|
||||
doDeleteStatus () {
|
||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||
this.hideDeleteStatusConfirmDialog()
|
||||
},
|
||||
showDeleteStatusConfirmDialog() {
|
||||
showDeleteStatusConfirmDialog () {
|
||||
this.showingDeleteDialog = true
|
||||
},
|
||||
hideDeleteStatusConfirmDialog() {
|
||||
hideDeleteStatusConfirmDialog () {
|
||||
this.showingDeleteDialog = false
|
||||
},
|
||||
translateStatus() {
|
||||
translateStatus () {
|
||||
if (this.noTranslationTargetSet) {
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'toast.no_translation_target_set',
|
||||
level: 'info'
|
||||
})
|
||||
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' })
|
||||
}
|
||||
const translateTo =
|
||||
this.$store.getters.mergedConfig.translationLanguage ||
|
||||
this.$store.state.instance.interfaceLanguage
|
||||
this.$store
|
||||
.dispatch('translateStatus', {
|
||||
id: this.status.id,
|
||||
language: translateTo
|
||||
})
|
||||
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
|
||||
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
pinStatus() {
|
||||
this.$store
|
||||
.dispatch('pinStatus', this.status.id)
|
||||
pinStatus () {
|
||||
this.$store.dispatch('pinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unpinStatus() {
|
||||
this.$store
|
||||
.dispatch('unpinStatus', this.status.id)
|
||||
unpinStatus () {
|
||||
this.$store.dispatch('unpinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
muteConversation() {
|
||||
this.$store
|
||||
.dispatch('muteConversation', this.status.id)
|
||||
muteConversation () {
|
||||
this.$store.dispatch('muteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unmuteConversation() {
|
||||
this.$store
|
||||
.dispatch('unmuteConversation', this.status.id)
|
||||
unmuteConversation () {
|
||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
copyLink() {
|
||||
navigator.clipboard
|
||||
.writeText(this.statusLink)
|
||||
copyLink () {
|
||||
navigator.clipboard.writeText(this.statusLink)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
bookmarkStatus() {
|
||||
this.$store
|
||||
.dispatch('bookmark', { id: this.status.id })
|
||||
bookmarkStatus () {
|
||||
this.$store.dispatch('bookmark', { id: this.status.id })
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unbookmarkStatus() {
|
||||
this.$store
|
||||
.dispatch('unbookmark', { id: this.status.id })
|
||||
unbookmarkStatus () {
|
||||
this.$store.dispatch('unbookmark', { id: this.status.id })
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
reportStatus() {
|
||||
this.$store.dispatch('openUserReportingModal', {
|
||||
userId: this.status.user.id,
|
||||
statusIds: [this.status.id]
|
||||
})
|
||||
reportStatus () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||
},
|
||||
editStatus() {
|
||||
this.$store
|
||||
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then((data) =>
|
||||
this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
})
|
||||
)
|
||||
editStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
},
|
||||
showStatusHistory() {
|
||||
showStatusHistory () {
|
||||
const originalStatus = { ...this.status }
|
||||
const stripFieldsList = [
|
||||
'attachments',
|
||||
'created_at',
|
||||
'emojis',
|
||||
'text',
|
||||
'raw_html',
|
||||
'nsfw',
|
||||
'poll',
|
||||
'summary',
|
||||
'summary_raw_html'
|
||||
]
|
||||
stripFieldsList.forEach((p) => delete originalStatus[p])
|
||||
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
|
||||
stripFieldsList.forEach(p => delete originalStatus[p])
|
||||
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
||||
},
|
||||
redraftStatus() {
|
||||
redraftStatus () {
|
||||
if (this.shouldConfirmDelete) {
|
||||
this.showRedraftStatusConfirmDialog()
|
||||
} else {
|
||||
this.doRedraftStatus()
|
||||
}
|
||||
},
|
||||
doRedraftStatus() {
|
||||
this.$store
|
||||
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then((data) =>
|
||||
doRedraftStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => {
|
||||
let repliedUserId = this.status.in_reply_to_user_id;
|
||||
let repliedUser = this.status.attentions.filter(user =>
|
||||
user.id === repliedUserId);
|
||||
this.$store.dispatch('openPostStatusModal', {
|
||||
isRedraft: true,
|
||||
attentions: this.status.attentions,
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
|
@ -180,70 +151,56 @@ const ExtraButtons = {
|
|||
statusFiles: [...this.status.attachments],
|
||||
statusScope: this.status.visibility,
|
||||
statusLanguage: this.status.language,
|
||||
statusContentType: data.content_type
|
||||
statusContentType: data.content_type,
|
||||
replyTo: this.status.in_reply_to_status_id,
|
||||
repliedUser: repliedUser
|
||||
})
|
||||
)
|
||||
})
|
||||
this.doDeleteStatus()
|
||||
},
|
||||
showRedraftStatusConfirmDialog() {
|
||||
showRedraftStatusConfirmDialog () {
|
||||
this.showingRedraftDialog = true
|
||||
},
|
||||
hideRedraftStatusConfirmDialog() {
|
||||
hideRedraftStatusConfirmDialog () {
|
||||
this.showingRedraftDialog = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
canDelete() {
|
||||
if (!this.currentUser) {
|
||||
return
|
||||
}
|
||||
const superuser =
|
||||
this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
canDelete () {
|
||||
if (!this.currentUser) { return }
|
||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
return superuser || this.status.user.id === this.currentUser.id
|
||||
},
|
||||
ownStatus() {
|
||||
ownStatus () {
|
||||
return this.status.user.id === this.currentUser.id
|
||||
},
|
||||
canPin() {
|
||||
return (
|
||||
this.ownStatus &&
|
||||
(this.status.visibility === 'public' ||
|
||||
this.status.visibility === 'unlisted')
|
||||
)
|
||||
canPin () {
|
||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||
},
|
||||
canMute() {
|
||||
canMute () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
canTranslate() {
|
||||
canTranslate () {
|
||||
return this.$store.state.instance.translationEnabled === true
|
||||
},
|
||||
noTranslationTargetSet() {
|
||||
noTranslationTargetSet () {
|
||||
return this.$store.getters.mergedConfig.translationLanguage === undefined
|
||||
},
|
||||
statusLink() {
|
||||
statusLink () {
|
||||
if (this.status.is_local) {
|
||||
return `${this.$store.state.instance.server}${
|
||||
this.$router.resolve({
|
||||
name: 'conversation',
|
||||
params: { id: this.status.id }
|
||||
}).href
|
||||
}`
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
} else {
|
||||
return this.status.external_url
|
||||
}
|
||||
},
|
||||
shouldConfirmDelete() {
|
||||
shouldConfirmDelete () {
|
||||
return this.$store.getters.mergedConfig.modalOnDelete
|
||||
},
|
||||
isEdited() {
|
||||
isEdited () {
|
||||
return this.status.edited_at !== null
|
||||
},
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
}
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content="{ close }">
|
||||
<template v-slot:content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -17,7 +17,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t('status.mute_conversation') }}</span>
|
||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canMute && status.thread_muted"
|
||||
|
@ -27,7 +27,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t('status.unmute_conversation') }}</span>
|
||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
|
@ -38,7 +38,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t('status.pin') }}</span>
|
||||
/><span>{{ $t("status.pin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
|
@ -49,7 +49,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t('status.unpin') }}</span>
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
|
@ -60,7 +60,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t('status.bookmark') }}</span>
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.bookmarked"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t('status.unbookmark') }}</span>
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus && editingAvailable"
|
||||
|
@ -82,7 +82,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="pen"
|
||||
/><span>{{ $t('status.edit') }}</span>
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isEdited && editingAvailable"
|
||||
|
@ -93,7 +93,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="history"
|
||||
/><span>{{ $t('status.edit_history') }}</span>
|
||||
/><span>{{ $t("status.edit_history") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus"
|
||||
|
@ -104,7 +104,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="file-pen"
|
||||
/><span>{{ $t('status.redraft') }}</span>
|
||||
/><span>{{ $t("status.redraft") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
|
@ -115,7 +115,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="times"
|
||||
/><span>{{ $t('status.delete') }}</span>
|
||||
/><span>{{ $t("status.delete") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -125,7 +125,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="share-alt"
|
||||
/><span>{{ $t('status.copy_link') }}</span>
|
||||
/><span>{{ $t("status.copy_link") }}</span>
|
||||
</button>
|
||||
<a
|
||||
v-if="!status.is_local"
|
||||
|
@ -137,7 +137,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="external-link-alt"
|
||||
/><span>{{ $t('status.external_source') }}</span>
|
||||
/><span>{{ $t("status.external_source") }}</span>
|
||||
</a>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -147,7 +147,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'flag']"
|
||||
/><span>{{ $t('user_card.report') }}</span>
|
||||
/><span>{{ $t("user_card.report") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canTranslate"
|
||||
|
@ -158,7 +158,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="globe"
|
||||
/><span>{{ $t('status.translate') }}</span>
|
||||
/><span>{{ $t("status.translate") }}</span>
|
||||
|
||||
<template v-if="noTranslationTargetSet">
|
||||
<span class="dropdown-item-icon__badge warning">
|
||||
|
@ -172,7 +172,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
@ -205,7 +205,7 @@
|
|||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js"></script>
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
|
||||
import {
|
||||
faStar as faStarRegular
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(faStar, faStarRegular)
|
||||
library.add(
|
||||
faStar,
|
||||
faStarRegular
|
||||
)
|
||||
|
||||
const FavoriteButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
animated: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
favorite() {
|
||||
favorite () {
|
||||
if (!this.status.favorited) {
|
||||
this.$store.dispatch('favorite', { id: this.status.id })
|
||||
} else {
|
||||
|
@ -27,10 +32,8 @@ const FavoriteButton = {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters(['mergedConfig']),
|
||||
remoteInteractionLink() {
|
||||
return this.$store.getters.remoteInteractionLink({
|
||||
statusId: this.status.id
|
||||
})
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./favorite_button.js"></script>
|
||||
<script src="./favorite_button.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
@ -55,8 +55,10 @@
|
|||
|
||||
.interactive {
|
||||
.svg-inline--fa {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: unset;
|
||||
}
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa,
|
||||
|
|
|
@ -2,20 +2,10 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
|
||||
const FeaturesPanel = {
|
||||
computed: {
|
||||
whoToFollow: function () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
mediaProxy: function () {
|
||||
return this.$store.state.instance.mediaProxyAvailable
|
||||
},
|
||||
textlimit: function () {
|
||||
return this.$store.state.instance.textlimit
|
||||
},
|
||||
uploadlimit: function () {
|
||||
return fileSizeFormatService.fileSizeFormat(
|
||||
this.$store.state.instance.uploadlimit
|
||||
)
|
||||
}
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,20 +16,17 @@
|
|||
</li>
|
||||
<li>{{ $t('features_panel.scope_options') }}</li>
|
||||
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
|
||||
<li>
|
||||
{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }}
|
||||
{{ $t('upload.file_size_units.' + uploadlimit.unit) }}
|
||||
</li>
|
||||
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./features_panel.js"></script>
|
||||
<script src="./features_panel.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
.features-panel li {
|
||||
line-height: 24px;
|
||||
}
|
||||
.features-panel li {
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue