forked from AkkomaGang/akkoma-fe
Compare commits
154 commits
fix-mute-b
...
develop
Author | SHA1 | Date | |
---|---|---|---|
4a50b1273d | |||
c76dc6d79e | |||
cb4c581cde | |||
|
8231c8f0b6 | ||
35cf3327c8 | |||
1ae09458c6 | |||
f391cf70a4 | |||
fa8fde2ab1 | |||
1f2c96a485 | |||
6c178aa257 | |||
|
3349fe6ff2 | ||
e274adf47d | |||
8765491399 | |||
4211e05a75 | |||
a3251a1ba6 | |||
e5608f4009 | |||
1092d43802 | |||
|
98a3622172 | ||
24b9e350e2 | |||
|
7ab4d22a4c | ||
8489f6d5ae | |||
b2cab6d088 | |||
3ebaba6fa7 | |||
f1058567b9 | |||
49a850a1e9 | |||
|
b2de68239f | ||
c68595345f | |||
a5d4b0a68c | |||
bd263587b2 | |||
|
c19fb198ca | ||
97966045cb | |||
aad023c8a0 | |||
c952b2335c | |||
0baf31f498 | |||
8fa24d0c40 | |||
5848c18ec8 | |||
|
54dbead22c | ||
3e86db24d3 | |||
|
d8f3f5002f | ||
7789c5def6 | |||
a45f482c79 | |||
|
ed22c480f9 | ||
f3934afbd8 | |||
|
3797495e53 | ||
0b437ab6fd | |||
a7dea2f70f | |||
|
2c9da4a58c | ||
8964dce609 | |||
|
156b036caa | ||
1a49a1b3ac | |||
|
61d82a2a07 | ||
1adef56603 | |||
|
b9bf0f0002 | ||
5aaa605df0 | |||
|
7136ea80b9 | ||
71e287d56c | |||
|
a64cdda725 | ||
70275684bf | |||
|
7e7f03aece | ||
29ff2ef455 | |||
8c49474dea | |||
62e0dd858c | |||
cc709394c5 | |||
57beea6a0d | |||
|
45524552a0 | ||
|
ee66b69ab5 | ||
|
d42e374704 | ||
|
ce8a9d2b4a | ||
|
d2b7ac6d8c | ||
|
754c72cb24 | ||
|
f5bd195422 | ||
|
d49fd46554 | ||
|
9982373853 | ||
|
5206b5cf9c | ||
|
a65a06ca04 | ||
|
c10b38afbc | ||
|
009941ea2c | ||
042e8c78dc | |||
0e07d88afa | |||
1f5f8665c8 | |||
428ed70b0d | |||
ed0b403c33 | |||
0f842b300b | |||
865cb6f96a | |||
050c7df2e6 | |||
a77a9e04d9 | |||
a57334991e | |||
8dce31d0ad | |||
ea9ad4d600 | |||
34e2800f59 | |||
3d65eccf04 | |||
d304be654f | |||
aee97fa948 | |||
7da1687f31 | |||
a8f193d4bd | |||
81c82e11bc | |||
00cadce5b4 | |||
40a08f279b | |||
c524a47e6f | |||
235c734d37 | |||
deaef1d0b9 | |||
1b28ec3b72 | |||
c9dc8f00f9 | |||
beee99e733 | |||
ccb0ffdc8a | |||
ab250c2f3a | |||
1de62fffcd | |||
306cea04a1 | |||
d9e1bc4d99 | |||
52b0b6f008 | |||
8afbe5e3bc | |||
58be48d164 | |||
1056b89fd1 | |||
3e64d78d05 | |||
3947aafeba | |||
345934c2f3 | |||
42a13b0f1b | |||
e13c4b6b85 | |||
6a1409e09b | |||
174f98b1cb | |||
ab146b67ec | |||
3b4208ea41 | |||
856324fa26 | |||
5a9322d2c7 | |||
|
b52bfbcba0 | ||
2b05a738c9 | |||
|
fa5d31b793 | ||
|
29cfdcbbcd | ||
|
5174b95918 | ||
|
56528206b8 | ||
|
590380c084 | ||
|
dfcbb3c1ec | ||
|
9b01c31283 | ||
|
6be003b2f8 | ||
|
cc302fb0e2 | ||
|
c4e768f977 | ||
|
d8a7217335 | ||
35d10ab9ff | |||
|
43d0a24547 | ||
|
51ebe643d5 | ||
7c14e1a5bd | |||
|
0da0e2c814 | ||
af97dd7484 | |||
28bf597443 | |||
a249baea8c | |||
a4e82f7886 | |||
|
32dc55b07c | ||
|
91eab22d77 | ||
|
e2125c57d6 | ||
|
e0a6418e91 | ||
|
14ed359c33 | ||
2d387e2eb4 | |||
57f70371a9 | |||
8feffbcdf6 |
174 changed files with 9169 additions and 6167 deletions
|
@ -1,2 +0,0 @@
|
||||||
build/*.js
|
|
||||||
config/*.js
|
|
30
.eslintrc.js
30
.eslintrc.js
|
@ -1,30 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@babel/eslint-parser',
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
|
||||||
extends: [
|
|
||||||
'plugin:vue/recommended'
|
|
||||||
],
|
|
||||||
// required to lint *.vue files
|
|
||||||
plugins: [
|
|
||||||
'vue',
|
|
||||||
'import'
|
|
||||||
],
|
|
||||||
// add your custom rules here
|
|
||||||
rules: {
|
|
||||||
// allow paren-less arrow functions
|
|
||||||
'arrow-parens': 0,
|
|
||||||
// allow async-await
|
|
||||||
'generator-star-spacing': 0,
|
|
||||||
// allow debugger during development
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
|
||||||
'vue/require-prop-types': 0,
|
|
||||||
'vue/no-unused-vars': 0,
|
|
||||||
'no-tabs': 0,
|
|
||||||
'vue/multi-word-component-names': 0,
|
|
||||||
'vue/no-reserved-component-names': 0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
7.2.1
|
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
nodejs 20.12.2
|
|
@ -1,19 +1,21 @@
|
||||||
pipeline:
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
lint:
|
lint:
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- pull_request
|
- pull_request
|
||||||
image: node:18
|
image: node:20
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
- yarn lint
|
- yarn lint
|
||||||
#- yarn stylelint
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- pull_request
|
- pull_request
|
||||||
image: node:18
|
image: node:20
|
||||||
commands:
|
commands:
|
||||||
- apt update
|
- apt update
|
||||||
- apt install firefox-esr -y --no-install-recommends
|
- apt install firefox-esr -y --no-install-recommends
|
||||||
|
@ -27,7 +29,7 @@ pipeline:
|
||||||
branch:
|
branch:
|
||||||
- develop
|
- develop
|
||||||
- stable
|
- stable
|
||||||
image: node:18
|
image: node:20
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
- yarn build
|
- yarn build
|
||||||
|
@ -39,15 +41,15 @@ pipeline:
|
||||||
branch:
|
branch:
|
||||||
- develop
|
- develop
|
||||||
- stable
|
- stable
|
||||||
image: node:18
|
image: node:20
|
||||||
secrets:
|
secrets:
|
||||||
- SCW_ACCESS_KEY
|
- SCW_ACCESS_KEY
|
||||||
- SCW_SECRET_KEY
|
- SCW_SECRET_KEY
|
||||||
- SCW_DEFAULT_ORGANIZATION_ID
|
- SCW_DEFAULT_ORGANIZATION_ID
|
||||||
commands:
|
commands:
|
||||||
- apt-get update && apt-get install -y rclone wget zip
|
- 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
|
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||||
- chmod +x scaleway-cli
|
- chmod +x scaleway-cli
|
||||||
- ./scaleway-cli object config install type=rclone
|
- ./scaleway-cli object config install type=rclone
|
||||||
- zip akkoma-fe.zip -r dist
|
- zip akkoma-fe.zip -r dist
|
||||||
|
@ -69,8 +71,8 @@ pipeline:
|
||||||
- SCW_DEFAULT_ORGANIZATION_ID
|
- SCW_DEFAULT_ORGANIZATION_ID
|
||||||
commands:
|
commands:
|
||||||
- apt-get update && apt-get install -y rclone wget git zip
|
- 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
|
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||||
- chmod +x scaleway-cli
|
- chmod +x scaleway-cli
|
||||||
- ./scaleway-cli object config install type=rclone
|
- ./scaleway-cli object config install type=rclone
|
||||||
- cd docs
|
- cd docs
|
||||||
|
|
|
@ -20,9 +20,11 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
|
||||||
|
|
||||||
## Build Setup
|
## Build Setup
|
||||||
|
|
||||||
|
Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.woodpecker.yml` for which node version the Akkoma CI currently uses.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# install dependencies
|
# install dependencies
|
||||||
npm install -g yarn
|
corepack enable
|
||||||
yarn
|
yarn
|
||||||
|
|
||||||
# serve with hot reload at localhost:8080
|
# serve with hot reload at localhost:8080
|
||||||
|
@ -37,7 +39,7 @@ npm run unit
|
||||||
|
|
||||||
# For Contributors:
|
# 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.
|
* `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.
|
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
// https://github.com/shelljs/shelljs
|
// https://github.com/shelljs/shelljs
|
||||||
require('./check-versions')()
|
require("./check-versions")();
|
||||||
require('shelljs/global')
|
require("shelljs/global");
|
||||||
env.NODE_ENV = 'production'
|
env.NODE_ENV = "production";
|
||||||
|
|
||||||
var path = require('path')
|
var path = require("path");
|
||||||
var config = require('../config')
|
var config = require("../config");
|
||||||
var ora = require('ora')
|
var webpack = require("webpack");
|
||||||
var webpack = require('webpack')
|
var webpackConfig = require("./webpack.prod.conf");
|
||||||
var webpackConfig = require('./webpack.prod.conf')
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
' Tip:\n' +
|
" Tip:\n" +
|
||||||
' Built files are meant to be served over an HTTP server.\n' +
|
" Built files are meant to be served over an HTTP server.\n" +
|
||||||
' Opening index.html over file:// won\'t work.\n'
|
" Opening index.html over file:// won't work.\n",
|
||||||
)
|
);
|
||||||
|
|
||||||
var spinner = ora('building for production...')
|
var assetsPath = path.join(
|
||||||
spinner.start()
|
config.build.assetsRoot,
|
||||||
|
config.build.assetsSubDirectory,
|
||||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
);
|
||||||
rm('-rf', assetsPath)
|
rm("-rf", assetsPath);
|
||||||
mkdir('-p', assetsPath)
|
mkdir("-p", assetsPath);
|
||||||
cp('-R', 'static/*', assetsPath)
|
cp("-R", "static/*", assetsPath);
|
||||||
|
|
||||||
webpack(webpackConfig, function (err, stats) {
|
webpack(webpackConfig, function (err, stats) {
|
||||||
spinner.stop()
|
if (err) throw err;
|
||||||
if (err) throw err
|
process.stdout.write(
|
||||||
process.stdout.write(stats.toString({
|
stats.toString({
|
||||||
colors: true,
|
colors: true,
|
||||||
modules: false,
|
modules: false,
|
||||||
children: false,
|
children: false,
|
||||||
chunks: false,
|
chunks: false,
|
||||||
chunkModules: false
|
chunkModules: false,
|
||||||
}) + '\n')
|
}) + "\n",
|
||||||
})
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ var path = require('path')
|
||||||
var express = require('express')
|
var express = require('express')
|
||||||
var webpack = require('webpack')
|
var webpack = require('webpack')
|
||||||
var opn = require('opn')
|
var opn = require('opn')
|
||||||
var proxyMiddleware = require('http-proxy-middleware')
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||||
? require('./webpack.prod.conf')
|
? require('./webpack.prod.conf')
|
||||||
: require('./webpack.dev.conf')
|
: require('./webpack.dev.conf')
|
||||||
|
@ -36,7 +36,13 @@ Object.keys(proxyTable).forEach(function (context) {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = { target: options }
|
options = { target: options }
|
||||||
}
|
}
|
||||||
app.use(proxyMiddleware(context, options))
|
const targetUrl = new URL(options.target);
|
||||||
|
// add path
|
||||||
|
targetUrl.pathname = context;
|
||||||
|
options.target = targetUrl.toString();
|
||||||
|
|
||||||
|
console.log("Proxying", context, "to", options.target);
|
||||||
|
app.use(context, createProxyMiddleware(options))
|
||||||
})
|
})
|
||||||
|
|
||||||
// handle fallback for HTML5 history API
|
// handle fallback for HTML5 history API
|
||||||
|
|
|
@ -3,6 +3,7 @@ var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
var { VueLoaderPlugin } = require('vue-loader')
|
var { VueLoaderPlugin } = require('vue-loader')
|
||||||
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
|
@ -35,6 +36,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
fallback: {
|
fallback: {
|
||||||
"url": require.resolve("url/"),
|
"url": require.resolve("url/"),
|
||||||
|
querystring: require.resolve("querystring-es3")
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
'static': path.resolve(__dirname, '../static'),
|
'static': path.resolve(__dirname, '../static'),
|
||||||
|
@ -47,20 +49,6 @@ module.exports = {
|
||||||
module: {
|
module: {
|
||||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
|
||||||
enforce: 'pre',
|
|
||||||
test: /\.(js|vue)$/,
|
|
||||||
include: projectRoot,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'eslint-loader',
|
|
||||||
options: {
|
|
||||||
formatter: require('eslint-friendly-formatter'),
|
|
||||||
sourceMap: config.build.productionSourceMap,
|
|
||||||
extract: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||||
|
@ -118,6 +106,9 @@ module.exports = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new VueLoaderPlugin()
|
new VueLoaderPlugin(),
|
||||||
|
new ESLintPlugin({
|
||||||
|
configType: 'flat'
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"target": "https://pleroma.soykaf.com/",
|
"target": "https://otp.akkoma.dev/",
|
||||||
"staticConfigPreference": false
|
"staticConfigPreference": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,4 @@ var { merge } = require('webpack-merge')
|
||||||
var devEnv = require('./dev.env')
|
var devEnv = require('./dev.env')
|
||||||
|
|
||||||
module.exports = merge(devEnv, {
|
module.exports = merge(devEnv, {
|
||||||
NODE_ENV: '"testing"'
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)*
|
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
|
||||||
|
|
||||||
## Where configuration is stored
|
## 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)
|
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/pleroma-fe/src/branch/develop/static/config.json).
|
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/pleroma-fe/src/branch/develop/src/modules/instance.js) )
|
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
|
## 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.
|
`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`
|
### `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`
|
### `nsfwCensorImage`
|
||||||
Use custom image for NSFW'd images
|
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
|
Show panel showcasing instance features/settings to logged-out visitors
|
||||||
|
|
||||||
### `showInstanceSpecificPanel`
|
### `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`
|
### `subjectLineBehavior`
|
||||||
How to handle subject line (CW) when replying to a post.
|
How to handle subject line (CW) when replying to a post.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Hacking, tweaking, contributing
|
# 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.
|
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
|
### 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
|
TODO: Refactor API code and document it here
|
||||||
|
|
||||||
### Themes
|
### 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.
|
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
|
# Introduction to Akkoma-FE
|
||||||
## What is Pleroma-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?
|
## 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
|
# 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.
|
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.
|
There's no explicit setting for these, they just rely on the existence of certain files.
|
||||||
|
|
||||||
## Initialising the sticker config file
|
## 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.
|
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`,
|
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`.
|
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
|
> --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
|
### Left column
|
||||||
|
|
||||||
- first block: This section is dedicated to [posting](posting_reading_basic_functions.md)
|
- 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.
|
- 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
|
- 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
|
### Right column
|
||||||
|
|
|
@ -15,11 +15,11 @@ 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...
|
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.
|
**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
|
Let's say there's a `:akkoma:` emoji defined on an instance. That means
|
||||||
> First time using :pleroma: pleroma!
|
> First time using :akkoma: akkoma!
|
||||||
|
|
||||||
will become
|
will become
|
||||||
> First time using ![pleroma](../assets/example_emoji.png) pleroma!
|
> 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.
|
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.
|
||||||
|
@ -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.
|
- 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.
|
* **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
|
## Rich text
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ If you set the input-method to Markdown, and post this, it will look something l
|
||||||
|
|
||||||
## Misskey markdown
|
## 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
|
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
|
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).
|
[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
|
## 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.
|
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.
|
**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).
|
**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:
|
theme:
|
||||||
favicon: 'images/pleroma_logo_vector_bg_32.png'
|
favicon: 'images/akkoma_logo_vector_bg_32.png'
|
||||||
name: 'material'
|
name: 'material'
|
||||||
custom_dir: 'theme'
|
custom_dir: 'theme'
|
||||||
# Disable google fonts
|
# Disable google fonts
|
||||||
font: false
|
font: false
|
||||||
logo: 'images/pleroma_logo_vector_nobg.svg'
|
logo: 'images/akkoma_logo_vector_nobg.svg'
|
||||||
features:
|
features:
|
||||||
- tabs
|
- tabs
|
||||||
palette:
|
palette:
|
||||||
|
@ -14,8 +14,8 @@ theme:
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
- css/extra.css
|
- css/extra.css
|
||||||
repo_name: 'AkkomaGang/pleroma-fe'
|
repo_name: 'AkkomaGang/akkoma-fe'
|
||||||
repo_url: 'https://akkoma.dev/AkkomaGang/pleroma-fe'
|
repo_url: 'https://akkoma.dev/AkkomaGang/akkoma-fe'
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
repo_icon: gitea
|
repo_icon: gitea
|
||||||
|
|
8
docs/theme/partials/source.html
vendored
8
docs/theme/partials/source.html
vendored
|
@ -38,11 +38,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page and page.url.startswith('backend') %}
|
{% if page and page.url.startswith('backend') %}
|
||||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
|
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma" %}
|
||||||
{% set repo_name = "pleroma/pleroma" %}
|
{% set repo_name = "AkkomaGang/akkoma" %}
|
||||||
{% elif page and page.url.startswith('frontend') %}
|
{% elif page and page.url.startswith('frontend') %}
|
||||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
|
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma-fe" %}
|
||||||
{% set repo_name = "pleroma/pleroma-fe" %}
|
{% set repo_name = "AkkomaGang/akkoma-fe" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set repo_url = config.repo_url %}
|
{% set repo_url = config.repo_url %}
|
||||||
{% set repo_name = config.repo_name %}
|
{% set repo_name = config.repo_name %}
|
||||||
|
|
31
eslint.config.js
Normal file
31
eslint.config.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const pluginVue = require('eslint-plugin-vue')
|
||||||
|
const pluginImport = require('eslint-plugin-import')
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
sourceType: 'module'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// allow paren-less arrow functions
|
||||||
|
'arrow-parens': 0,
|
||||||
|
// allow async-await
|
||||||
|
'generator-star-spacing': 0,
|
||||||
|
// allow debugger during development
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||||
|
'vue/require-prop-types': 0,
|
||||||
|
'vue/no-unused-vars': 0,
|
||||||
|
'no-tabs': 0,
|
||||||
|
'vue/multi-word-component-names': 0,
|
||||||
|
'vue/no-reserved-component-names': 0
|
||||||
|
},
|
||||||
|
ignores: [
|
||||||
|
'build/*.js',
|
||||||
|
'config/*.js'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
164
package.json
164
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pleroma_fe",
|
"name": "pleroma_fe",
|
||||||
"version": "3.5.0",
|
"version": "3.10.0",
|
||||||
"description": "A frontend for Akkoma instances",
|
"description": "A frontend for Akkoma instances",
|
||||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -12,120 +12,118 @@
|
||||||
"e2e": "node test/e2e/runner.js",
|
"e2e": "node test/e2e/runner.js",
|
||||||
"test": "npm run unit && npm run e2e",
|
"test": "npm run unit && npm run e2e",
|
||||||
"stylelint": "stylelint src/**/*.scss",
|
"stylelint": "stylelint src/**/*.scss",
|
||||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
"lint": "eslint src test/unit/specs test/e2e/specs",
|
||||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.17.8",
|
"@babel/runtime": "7.17.8",
|
||||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
"@chenfengyuan/vue-qrcode": "^2.0.0",
|
||||||
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
"@vuelidate/core": "^2.0.0",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "^2.0.0",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"blurhash": "^2.0.4",
|
"blurhash": "^2.0.5",
|
||||||
"body-scroll-lock": "2.7.1",
|
"body-scroll-lock": "^3.1.5",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "^3.0.0",
|
||||||
"click-outside-vue3": "4.0.1",
|
"click-outside-vue3": "^4.0.1",
|
||||||
"cropperjs": "1.5.12",
|
"cropperjs": "^1.6.2",
|
||||||
"diff": "3.5.0",
|
"diff": "^5.2.0",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"iso-639-1": "^2.1.15",
|
"iso-639-1": "^2.1.15",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"parse-link-header": "^2.0.0",
|
"parse-link-header": "^2.0.0",
|
||||||
"phoenix": "1.6.2",
|
"phoenix": "^1.7.12",
|
||||||
"punycode.js": "2.1.0",
|
"punycode.js": "^2.3.1",
|
||||||
"qrcode": "1",
|
"qrcode": "^1.5.3",
|
||||||
"url": "^0.11.0",
|
"querystring-es3": "^0.2.1",
|
||||||
"vue": "^3.2.31",
|
"url": "^0.11.3",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue": "^3.4.38",
|
||||||
"vue-router": "4.0.14",
|
"vue-i18n": "^9.14.0",
|
||||||
"vue-template-compiler": "2.6.11",
|
"vue-router": "^4.4.3",
|
||||||
"vuex": "4.0.2"
|
"vue-template-compiler": "^2.7.16",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.17.8",
|
"@babel/core": "^7.24.6",
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@babel/plugin-transform-runtime": "7.17.0",
|
"@babel/plugin-transform-runtime": "^7.24.6",
|
||||||
"@babel/preset-env": "7.16.11",
|
"@babel/preset-env": "^7.24.6",
|
||||||
"@babel/register": "7.17.7",
|
"@babel/register": "^7.24.6",
|
||||||
"@intlify/vue-i18n-loader": "^5.0.0",
|
"@intlify/vue-i18n-loader": "^5.0.0",
|
||||||
"@ungap/event-target": "0.2.3",
|
"@ungap/event-target": "^0.2.4",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
|
||||||
"@vue/babel-plugin-jsx": "1.1.1",
|
"@vue/babel-plugin-jsx": "^1.2.2",
|
||||||
"@vue/compiler-sfc": "^3.1.0",
|
"@vue/compiler-sfc": "^3.1.0",
|
||||||
"@vue/test-utils": "^2.0.2",
|
"@vue/test-utils": "^2.0.2",
|
||||||
"autoprefixer": "6.7.7",
|
"autoprefixer": "^10.4.19",
|
||||||
"babel-loader": "^9.1.0",
|
"babel-loader": "^9.1.0",
|
||||||
"babel-plugin-lodash": "3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"chalk": "1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"chromedriver": "^107.0.3",
|
"chromedriver": "^119.0.1",
|
||||||
"connect-history-api-fallback": "^2.0.0",
|
"connect-history-api-fallback": "^2.0.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"css-loader": "^6.7.2",
|
"css-loader": "^7.1.2",
|
||||||
"custom-event-polyfill": "^1.0.7",
|
"custom-event-polyfill": "^1.0.7",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^9.3.0",
|
||||||
"eslint-config-standard": "^17.0.0",
|
"eslint-config-standard": "^17.1.0",
|
||||||
"eslint-friendly-formatter": "^4.0.1",
|
"eslint-friendly-formatter": "^4.0.1",
|
||||||
"eslint-loader": "^4.0.2",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
"eslint-plugin-promise": "^6.2.0",
|
||||||
"eslint-plugin-standard": "^5.0.0",
|
"eslint-plugin-standard": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.7.0",
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
"eventsource-polyfill": "0.9.6",
|
"eslint-webpack-plugin": "^4.2.0",
|
||||||
"express": "4.17.3",
|
"eventsource-polyfill": "^0.9.6",
|
||||||
|
"express": "^4.19.2",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"function-bind": "1.1.1",
|
"function-bind": "^1.1.2",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"http-proxy-middleware": "0.21.0",
|
"http-proxy-middleware": "^3.0.0",
|
||||||
"inject-loader": "2.0.1",
|
"json-loader": "^0.5.7",
|
||||||
"isparta-loader": "2.0.0",
|
"karma": "^6.4.3",
|
||||||
"json-loader": "0.5.7",
|
"karma-coverage": "^2.2.1",
|
||||||
"karma": "6.3.17",
|
"karma-firefox-launcher": "^2.1.3",
|
||||||
"karma-coverage": "1.1.2",
|
"karma-mocha": "^2.0.1",
|
||||||
"karma-firefox-launcher": "1.3.0",
|
"karma-mocha-reporter": "^2.2.5",
|
||||||
"karma-mocha": "2.0.1",
|
"karma-sinon-chai": "^2.0.2",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-sourcemap-loader": "^0.4.0",
|
||||||
"karma-sinon-chai": "2.0.2",
|
"karma-spec-reporter": "^0.0.36",
|
||||||
"karma-sourcemap-loader": "0.3.8",
|
|
||||||
"karma-spec-reporter": "0.0.33",
|
|
||||||
"karma-webpack": "^5.0.0",
|
"karma-webpack": "^5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lolex": "1.6.0",
|
"lolex": "^6.0.0",
|
||||||
"mini-css-extract-plugin": "0.12.0",
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
"mocha": "3.5.3",
|
"mocha": "^10.4.0",
|
||||||
"nightwatch": "0.9.21",
|
"nightwatch": "^3.6.3",
|
||||||
"opn": "4.0.2",
|
"opn": "^6.0.0",
|
||||||
"ora": "0.4.1",
|
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "^8.1.1",
|
||||||
"postcss-sass": "^0.5.0",
|
"postcss-sass": "^0.5.0",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "^4.0.2",
|
||||||
"sass": "^1.56.0",
|
"sass": "^1.77.2",
|
||||||
"sass-loader": "^13.2.0",
|
"sass-loader": "^14.2.1",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "^3.141.59",
|
||||||
"semver": "5.7.1",
|
"semver": "^7.6.2",
|
||||||
"shelljs": "0.8.5",
|
"shelljs": "^0.8.5",
|
||||||
"sinon": "2.4.1",
|
"sinon": "^18.0.0",
|
||||||
"sinon-chai": "2.14.0",
|
"sinon-chai": "^3.7.0",
|
||||||
"stylelint": "^14.15.0",
|
"stylelint": "^14.15.0",
|
||||||
"stylelint-config-recommended-vue": "^1.4.0",
|
"stylelint-config-recommended-vue": "^1.4.0",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-config-standard-scss": "^6.1.0",
|
"stylelint-config-standard-scss": "^6.1.0",
|
||||||
"stylelint-rscss": "^0.4.0",
|
"stylelint-rscss": "^0.4.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-loader": "^17.0.0",
|
"vue-loader": "^17.4.2",
|
||||||
"vue-style-loader": "^4.1.2",
|
"vue-style-loader": "^4.1.3",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.91.0",
|
||||||
"webpack-dev-middleware": "^5.3.3",
|
"webpack-dev-middleware": "^7.2.1",
|
||||||
"webpack-hot-middleware": "^2.25.1",
|
"webpack-hot-middleware": "^2.26.1",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.10.0",
|
||||||
"workbox-webpack-plugin": "^6.5.4"
|
"workbox-webpack-plugin": "^7.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16.0.0",
|
"node": ">= 16.0.0",
|
||||||
|
|
|
@ -64,6 +64,11 @@ export default {
|
||||||
'-' + this.layoutType
|
'-' + this.layoutType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
pageBackground () {
|
||||||
|
return this.mergedConfig.displayPageBackgrounds
|
||||||
|
? this.$store.state.users.displayBackground
|
||||||
|
: null
|
||||||
|
},
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
userBackground () { return this.currentUser.background_image },
|
userBackground () { return this.currentUser.background_image },
|
||||||
instanceBackground () {
|
instanceBackground () {
|
||||||
|
@ -71,7 +76,7 @@ export default {
|
||||||
? null
|
? null
|
||||||
: this.$store.state.instance.background
|
: this.$store.state.instance.background
|
||||||
},
|
},
|
||||||
background () { return this.userBackground || this.instanceBackground },
|
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
|
||||||
bgStyle () {
|
bgStyle () {
|
||||||
if (this.background) {
|
if (this.background) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="relationship.following">
|
<template v-if="relationship.following">
|
||||||
<button
|
<button
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button class="button-unstyled ellipsis-button">
|
<button class="button-unstyled ellipsis-button">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="icon"
|
class="icon"
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
keypath="user_card.block_confirm"
|
keypath="user_card.block_confirm"
|
||||||
tag="span"
|
tag="span"
|
||||||
>
|
>
|
||||||
<template v-slot:user>
|
<template #user>
|
||||||
<span
|
<span
|
||||||
v-text="user.screen_name_ui"
|
v-text="user.screen_name_ui"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-static {
|
&.-static {
|
||||||
|
|
|
@ -246,8 +246,8 @@
|
||||||
ref="flash"
|
ref="flash"
|
||||||
class="flash"
|
class="flash"
|
||||||
:src="attachment.large_thumb_url || attachment.url"
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
@playerOpened="setFlashLoaded(true)"
|
@player-opened="setFlashLoaded(true)"
|
||||||
@playerClosed="setFlashLoaded(false)"
|
@player-closed="setFlashLoaded(false)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:modelValue'],
|
|
||||||
props: [
|
props: [
|
||||||
'modelValue',
|
'modelValue',
|
||||||
'indeterminate',
|
'indeterminate',
|
||||||
'disabled'
|
'disabled'
|
||||||
]
|
],
|
||||||
|
emits: ['update:modelValue']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
:model-value="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
class="opt"
|
||||||
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
@update:model-value="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
/>
|
/>
|
||||||
<div class="input color-input-field">
|
<div class="input color-input-field">
|
||||||
<input
|
<input
|
||||||
|
@ -46,7 +46,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" src="./color_input.scss"></style>
|
|
||||||
<script>
|
<script>
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
|
@ -108,6 +107,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" src="./color_input.scss"></style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.color-control {
|
.color-control {
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
</dialog-modal>
|
</dialog-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script src="./confirm_modal.js"></script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../_variables';
|
@import '../../_variables';
|
||||||
|
|
||||||
|
@ -35,5 +37,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script src="./confirm_modal.js"></script>
|
|
||||||
|
|
|
@ -267,11 +267,11 @@ const conversation = {
|
||||||
},
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
||||||
/* eslint-disable camelcase */
|
|
||||||
const irid = in_reply_to_status_id
|
const irid = in_reply_to_status_id
|
||||||
/* eslint-enable camelcase */
|
|
||||||
if (irid) {
|
if (irid) {
|
||||||
result[irid] = result[irid] || []
|
result[irid] = result[irid] || []
|
||||||
result[irid].push({
|
result[irid].push({
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
@ -184,7 +184,7 @@
|
||||||
:toggle-status-content-property="toggleStatusContentProperty"
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,9 +44,9 @@
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="publicTimelineVisible"
|
||||||
:to="{ name: 'public-timeline' }"
|
:to="{ name: 'public-timeline' }"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
v-if="publicTimelineVisible"
|
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
|
@ -68,9 +68,9 @@
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="federatedTimelineVisible"
|
||||||
:to="{ name: 'public-external-timeline' }"
|
:to="{ name: 'public-external-timeline' }"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
v-if="federatedTimelineVisible"
|
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.mute') }}
|
{{ $t('domain_mute_card.mute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('domain_mute_card.mute_progress') }}
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<Modal
|
<Modal
|
||||||
v-if="isFormVisible"
|
v-if="isFormVisible"
|
||||||
class="edit-form-modal-view"
|
class="edit-form-modal-view"
|
||||||
@backdropClicked="closeModal"
|
@backdrop-clicked="closeModal"
|
||||||
>
|
>
|
||||||
<div class="edit-form-modal-panel panel">
|
<div class="edit-form-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<PostStatusForm
|
<PostStatusForm
|
||||||
class="panel-body"
|
class="panel-body"
|
||||||
v-bind="params"
|
v-bind="params"
|
||||||
@posted="closeModal"
|
:disable-polls="true"
|
||||||
:disablePolls="true"
|
:disable-visibility-selector="true"
|
||||||
:disableVisibilitySelector="true"
|
|
||||||
:post-handler="doEditStatus"
|
:post-handler="doEditStatus"
|
||||||
|
@posted="closeModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -43,7 +43,10 @@
|
||||||
:class="{ highlighted: index === highlighted }"
|
:class="{ highlighted: index === highlighted }"
|
||||||
@click.stop.prevent="onClick($event, suggestion)"
|
@click.stop.prevent="onClick($event, suggestion)"
|
||||||
>
|
>
|
||||||
<span v-if="!suggestion.mfm" class="image">
|
<span
|
||||||
|
v-if="!suggestion.mfm"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="suggestion.img"
|
v-if="suggestion.img"
|
||||||
:src="suggestion.img"
|
:src="suggestion.img"
|
||||||
|
|
|
@ -122,14 +122,14 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||||
|
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
/* eslint-disable camelcase */
|
|
||||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||||
displayText: screen_name_ui,
|
displayText: screen_name_ui,
|
||||||
detailText: name,
|
detailText: name,
|
||||||
imageUrl: profile_image_url_original,
|
imageUrl: profile_image_url_original,
|
||||||
replacement: '@' + screen_name + ' '
|
replacement: '@' + screen_name + ' '
|
||||||
}))
|
}))
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
suggestions = newSuggestions || []
|
suggestions = newSuggestions || []
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { trim, escapeRegExp, startCase } from 'lodash'
|
import { trim, escapeRegExp, startCase, debounce } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
@ -42,6 +42,9 @@ const EmojiPicker = {
|
||||||
EmojiGrid
|
EmojiGrid
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
debouncedSearch: debounce(function (e) {
|
||||||
|
this.keyword = e.target.value
|
||||||
|
}, 500),
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
|
@ -85,17 +88,6 @@ const EmojiPicker = {
|
||||||
activeGroupView () {
|
activeGroupView () {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
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 recentEmojis = this.$store.getters.recentEmojis
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
const standardEmojis = this.$store.state.instance.emoji || []
|
||||||
|
|
|
@ -44,11 +44,10 @@
|
||||||
>
|
>
|
||||||
<div class="emoji-search">
|
<div class="emoji-search">
|
||||||
<input
|
<input
|
||||||
v-model="keyword"
|
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="$t('emoji.search_emoji')"
|
:placeholder="$t('emoji.search_emoji')"
|
||||||
@input="$event.target.composing = false"
|
@input="debouncedSearch"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<EmojiGrid
|
<EmojiGrid
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserListPopover from '../user_list_popover/user_list_popover.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 EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||||
|
|
||||||
|
@ -12,7 +13,8 @@ const EmojiReactions = {
|
||||||
name: 'EmojiReactions',
|
name: 'EmojiReactions',
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserListPopover
|
UserListPopover,
|
||||||
|
StillImage
|
||||||
},
|
},
|
||||||
props: ['status'],
|
props: ['status'],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
|
@ -14,12 +14,13 @@
|
||||||
<span
|
<span
|
||||||
v-if="reaction.url !== null"
|
v-if="reaction.url !== null"
|
||||||
>
|
>
|
||||||
<img
|
<StillImage
|
||||||
:src="reaction.url"
|
:src="reaction.url"
|
||||||
:title="reaction.name"
|
:title="reaction.name"
|
||||||
|
:alt="reaction.name"
|
||||||
class="reaction-emoji"
|
class="reaction-emoji"
|
||||||
height="2.55em"
|
height="2.55em"
|
||||||
>
|
/>
|
||||||
{{ reaction.count }}
|
{{ reaction.count }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
|
|
@ -136,8 +136,13 @@ const ExtraButtons = {
|
||||||
},
|
},
|
||||||
doRedraftStatus () {
|
doRedraftStatus () {
|
||||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||||
.then(data => this.$store.dispatch('openPostStatusModal', {
|
.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,
|
isRedraft: true,
|
||||||
|
attentions: this.status.attentions,
|
||||||
statusId: this.status.id,
|
statusId: this.status.id,
|
||||||
subject: data.spoiler_text,
|
subject: data.spoiler_text,
|
||||||
statusText: data.text,
|
statusText: data.text,
|
||||||
|
@ -146,8 +151,11 @@ const ExtraButtons = {
|
||||||
statusFiles: [...this.status.attachments],
|
statusFiles: [...this.status.attachments],
|
||||||
statusScope: this.status.visibility,
|
statusScope: this.status.visibility,
|
||||||
statusLanguage: this.status.language,
|
statusLanguage: this.status.language,
|
||||||
statusContentType: data.content_type
|
statusContentType: data.content_type,
|
||||||
}))
|
replyTo: this.status.in_reply_to_status_id,
|
||||||
|
repliedUser: repliedUser
|
||||||
|
})
|
||||||
|
})
|
||||||
this.doDeleteStatus()
|
this.doDeleteStatus()
|
||||||
},
|
},
|
||||||
showRedraftStatusConfirmDialog () {
|
showRedraftStatusConfirmDialog () {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
>
|
>
|
||||||
<template v-slot:content="{close}">
|
<template #content="{close}">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
v-if="canMute && !status.thread_muted"
|
v-if="canMute && !status.thread_muted"
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button class="button-unstyled popover-trigger">
|
<button class="button-unstyled popover-trigger">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
|
|
@ -55,6 +55,9 @@
|
||||||
|
|
||||||
.interactive {
|
.interactive {
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
animation: unset;
|
||||||
|
}
|
||||||
animation-duration: 0.6s;
|
animation-duration: 0.6s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<basic-user-card :user="user" v-if="show">
|
<basic-user-card
|
||||||
|
v-if="show"
|
||||||
|
:user="user"
|
||||||
|
>
|
||||||
<div class="follow-request-card-content-container">
|
<div class="follow-request-card-content-container">
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
|
|
@ -31,8 +31,8 @@
|
||||||
:description="descriptions && descriptions[attachment.id]"
|
:description="descriptions && descriptions[attachment.id]"
|
||||||
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
||||||
:style="itemStyle(attachment.id, row.items)"
|
:style="itemStyle(attachment.id, row.items)"
|
||||||
@setMedia="onMedia"
|
@set-media="onMedia"
|
||||||
@naturalSizeLoad="onNaturalSizeLoad"
|
@natural-size-load="onNaturalSizeLoad"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="list">
|
<div
|
||||||
|
class="list"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="getKey(item)"
|
:key="getKey(item)"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
|
role="listitem"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="item"
|
name="item"
|
||||||
|
@ -38,6 +42,7 @@ export default {
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
min-height: 1em;
|
||||||
&-item:not(:last-child) {
|
&-item:not(:last-child) {
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-bottom-color: $fallback--border;
|
border-bottom-color: $fallback--border;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<Modal
|
<Modal
|
||||||
v-if="showing"
|
v-if="showing"
|
||||||
class="media-modal-view"
|
class="media-modal-view"
|
||||||
@backdropClicked="hideIfNotSwiped"
|
@backdrop-clicked="hideIfNotSwiped"
|
||||||
>
|
>
|
||||||
<SwipeClick
|
<SwipeClick
|
||||||
v-if="type === 'image'"
|
v-if="type === 'image'"
|
||||||
|
|
|
@ -42,7 +42,7 @@ const mediaUpload = {
|
||||||
.then((fileData) => {
|
.then((fileData) => {
|
||||||
self.$emit('uploaded', fileData)
|
self.$emit('uploaded', fileData)
|
||||||
self.decreaseUploadCount()
|
self.decreaseUploadCount()
|
||||||
}, (error) => { // eslint-disable-line handle-callback-err
|
}, (error) => {
|
||||||
self.$emit('upload-failed', 'default')
|
self.$emit('upload-failed', 'default')
|
||||||
self.decreaseUploadCount()
|
self.decreaseUploadCount()
|
||||||
})
|
})
|
||||||
|
|
|
@ -93,9 +93,6 @@ const MentionLink = {
|
||||||
this.highlightType
|
this.highlightType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
useAtIcon () {
|
|
||||||
return this.mergedConfig.useAtIcon
|
|
||||||
},
|
|
||||||
isRemote () {
|
isRemote () {
|
||||||
return this.userName !== this.userNameFull
|
return this.userName !== this.userNameFull
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<input
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
v-model="code"
|
v-model="code"
|
||||||
|
autocomplete="one-time-code"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -157,6 +157,9 @@
|
||||||
box-shadow: var(--panelShadow);
|
box-shadow: var(--panelShadow);
|
||||||
transition-property: transform;
|
transition-property: transform;
|
||||||
transition-duration: 0.25s;
|
transition-duration: 0.25s;
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
class="panel-heading"
|
class="panel-heading"
|
||||||
@click="toggleHidden"
|
@click="toggleHidden"
|
||||||
>
|
>
|
||||||
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
|
<h4>{{ $t('moderation.reports.report') + ' ' + account.screen_name }}</h4>
|
||||||
<button
|
<button
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="button-default"
|
class="button-default"
|
||||||
|
@ -35,7 +35,10 @@
|
||||||
<div v-if="content">
|
<div v-if="content">
|
||||||
{{ decode(content) }}
|
{{ decode(content) }}
|
||||||
</div>
|
</div>
|
||||||
<i v-else class="faint">
|
<i
|
||||||
|
v-else
|
||||||
|
class="faint"
|
||||||
|
>
|
||||||
{{ $t('moderation.reports.no_content') }}
|
{{ $t('moderation.reports.no_content') }}
|
||||||
</i>
|
</i>
|
||||||
<div class="report-author">
|
<div class="report-author">
|
||||||
|
@ -43,12 +46,12 @@
|
||||||
class="small-avatar"
|
class="small-avatar"
|
||||||
:user="actor"
|
:user="actor"
|
||||||
/>
|
/>
|
||||||
{{ this.actor.screen_name }}
|
{{ actor.screen_name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="!hidden && statuses.length > 0"
|
||||||
class="dropdown"
|
class="dropdown"
|
||||||
v-if="!hidden && this.statuses.length > 0"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button button-unstyled dropdown-header"
|
class="button button-unstyled dropdown-header"
|
||||||
|
@ -74,8 +77,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="!hidden && notes.length > 0"
|
||||||
class="dropdown"
|
class="dropdown"
|
||||||
v-if="!hidden && this.notes.length > 0"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button button-unstyled dropdown-header"
|
class="button button-unstyled dropdown-header"
|
||||||
|
@ -99,9 +102,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="report-add-note">
|
<div class="report-add-note">
|
||||||
<textarea
|
<textarea
|
||||||
|
v-model.trim="note"
|
||||||
rows="1"
|
rows="1"
|
||||||
cols="1"
|
cols="1"
|
||||||
v-model.trim="note"
|
|
||||||
:placeholder="$t('moderation.reports.note_placeholder')"
|
:placeholder="$t('moderation.reports.note_placeholder')"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -134,7 +137,7 @@
|
||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
remove-padding
|
remove-padding
|
||||||
>
|
>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
:disabled="!tagPolicyEnabled"
|
:disabled="!tagPolicyEnabled"
|
||||||
|
@ -147,7 +150,7 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:content="{close}">
|
<template #content="{close}">
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu"
|
class="dropdown-menu"
|
||||||
:disabled="!tagPolicyEnabled"
|
:disabled="!tagPolicyEnabled"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
class="small-avatar"
|
class="small-avatar"
|
||||||
:user="user"
|
:user="user"
|
||||||
/>
|
/>
|
||||||
{{ this.user.screen_name }}
|
{{ user.screen_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<Timeago
|
<Timeago
|
||||||
|
|
|
@ -22,6 +22,9 @@ export default {
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: [
|
||||||
|
'backdropClicked',
|
||||||
|
],
|
||||||
computed: {
|
computed: {
|
||||||
classes () {
|
classes () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@show="setToggled(true)"
|
@show="setToggled(true)"
|
||||||
@close="setToggled(false)"
|
@close="setToggled(false)"
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<span v-if="user.is_local">
|
<span v-if="user.is_local">
|
||||||
<button
|
<button
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="btn button-default btn-block moderation-tools-button"
|
class="btn button-default btn-block moderation-tools-button"
|
||||||
:class="{ toggled }"
|
:class="{ toggled }"
|
||||||
|
@ -137,11 +137,11 @@
|
||||||
v-if="showDeleteUserDialog"
|
v-if="showDeleteUserDialog"
|
||||||
:on-cancel="deleteUserDialog.bind(this, false)"
|
:on-cancel="deleteUserDialog.bind(this, false)"
|
||||||
>
|
>
|
||||||
<template v-slot:header>
|
<template #header>
|
||||||
{{ $t('user_card.admin_menu.delete_user') }}
|
{{ $t('user_card.admin_menu.delete_user') }}
|
||||||
</template>
|
</template>
|
||||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||||
<template v-slot:footer>
|
<template #footer>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
@click="deleteUserDialog(false)"
|
@click="deleteUserDialog(false)"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import UserCard from '../user_card/user_card.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||||
|
import StillImage from '../still-image/still-image.vue'
|
||||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
@ -50,7 +51,8 @@ const Notification = {
|
||||||
Timeago,
|
Timeago,
|
||||||
Status,
|
Status,
|
||||||
RichContent,
|
RichContent,
|
||||||
ConfirmModal
|
ConfirmModal,
|
||||||
|
StillImage
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
|
|
@ -116,12 +116,13 @@
|
||||||
scope="global"
|
scope="global"
|
||||||
keypath="notifications.reacted_with"
|
keypath="notifications.reacted_with"
|
||||||
>
|
>
|
||||||
<img
|
<still-image
|
||||||
v-if="notification.emoji_url !== null"
|
v-if="notification.emoji_url !== null"
|
||||||
class="notification-reaction-emoji"
|
class="notification-reaction-emoji"
|
||||||
:src="notification.emoji_url"
|
:src="notification.emoji_url"
|
||||||
:name="notification.emoji"
|
:title="notification.emoji"
|
||||||
>
|
:alt="notification.emoji"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="emoji-reaction-emoji"
|
class="emoji-reaction-emoji"
|
||||||
|
@ -151,7 +152,6 @@
|
||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="notification.created_at"
|
:time="notification.created_at"
|
||||||
:with-direction="true"
|
|
||||||
:auto-update="240"
|
:auto-update="240"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button class="filter-trigger-button button-unstyled">
|
<button class="filter-trigger-button button-unstyled">
|
||||||
<FAIcon icon="filter" />
|
<FAIcon icon="filter" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -105,9 +105,12 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-left: 0.8em;
|
padding-left: 0.8em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-right, .notification-right {
|
||||||
.timeago {
|
.timeago {
|
||||||
min-width: 3em;
|
display: inline-block;
|
||||||
|
min-width: 6em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
:model-value="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
class="opt"
|
||||||
@update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
|
@update:model-value="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name"
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<pinch-zoom
|
<pinch-zoom
|
||||||
class="pinch-zoom-parent"
|
class="pinch-zoom-parent"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-on="$listeners"
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</pinch-zoom>
|
</pinch-zoom>
|
||||||
|
|
|
@ -9,11 +9,12 @@ import StatusContent from '../status_content/status_content.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
import { reject, map, uniqBy, debounce } from 'lodash'
|
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||||
|
import { usePostLanguageOptions } from 'src/lib/post_language'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
import iso6391 from 'iso-639-1'
|
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -62,6 +63,13 @@ const deleteDraft = (draftKey) => {
|
||||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const interfaceToISOLanguage = (ilang) => {
|
||||||
|
const sep = ilang.indexOf("_");
|
||||||
|
return sep < 0 ?
|
||||||
|
ilang :
|
||||||
|
ilang.substr(0, sep);
|
||||||
|
}
|
||||||
|
|
||||||
const PostStatusForm = {
|
const PostStatusForm = {
|
||||||
props: [
|
props: [
|
||||||
'statusId',
|
'statusId',
|
||||||
|
@ -129,6 +137,13 @@ const PostStatusForm = {
|
||||||
this.$refs.textarea.focus()
|
this.$refs.textarea.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const {postLanguageOptions} = usePostLanguageOptions()
|
||||||
|
|
||||||
|
return {
|
||||||
|
postLanguageOptions,
|
||||||
|
}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
const preset = this.$route.query.message
|
const preset = this.$route.query.message
|
||||||
let statusText = preset || ''
|
let statusText = preset || ''
|
||||||
|
@ -138,7 +153,8 @@ const PostStatusForm = {
|
||||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
|
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||||
|
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
|
||||||
|
|
||||||
let statusParams = {
|
let statusParams = {
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
|
@ -149,7 +165,7 @@ const PostStatusForm = {
|
||||||
poll: {},
|
poll: {},
|
||||||
mediaDescriptions: {},
|
mediaDescriptions: {},
|
||||||
visibility: this.suggestedVisibility(),
|
visibility: this.suggestedVisibility(),
|
||||||
language: interfaceLanguage,
|
language: postLanguage,
|
||||||
contentType
|
contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +180,7 @@ const PostStatusForm = {
|
||||||
poll: this.statusPoll || {},
|
poll: this.statusPoll || {},
|
||||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||||
visibility: this.statusScope || this.suggestedVisibility(),
|
visibility: this.statusScope || this.suggestedVisibility(),
|
||||||
language: this.statusLanguage || interfaceLanguage,
|
language: this.statusLanguage || postLanguage,
|
||||||
contentType: statusContentType
|
contentType: statusContentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,6 +215,10 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When first loading the form, hide the subject (CW) field if it's disabled or doesn't have a starting value.
|
||||||
|
// "disableSubject" seems to take priority over "alwaysShowSubjectInput".
|
||||||
|
const showSubject = !this.disableSubject && (statusParams.spoilerText || alwaysShowSubjectInput)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropFiles: [],
|
dropFiles: [],
|
||||||
uploadingFiles: false,
|
uploadingFiles: false,
|
||||||
|
@ -213,7 +233,10 @@ const PostStatusForm = {
|
||||||
preview: null,
|
preview: null,
|
||||||
previewLoading: false,
|
previewLoading: false,
|
||||||
emojiInputShown: false,
|
emojiInputShown: false,
|
||||||
idempotencyKey: ''
|
idempotencyKey: '',
|
||||||
|
activeEmojiInput: undefined,
|
||||||
|
activeTextInput: undefined,
|
||||||
|
subjectVisible: showSubject
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -302,9 +325,6 @@ const PostStatusForm = {
|
||||||
...mapState({
|
...mapState({
|
||||||
mobileLayout: state => state.interface.mobileLayout
|
mobileLayout: state => state.interface.mobileLayout
|
||||||
}),
|
}),
|
||||||
isoLanguages () {
|
|
||||||
return iso6391.getAllCodes();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'newStatus': {
|
'newStatus': {
|
||||||
|
@ -674,8 +694,33 @@ const PostStatusForm = {
|
||||||
this.$refs['emoji-input'].resize()
|
this.$refs['emoji-input'].resize()
|
||||||
},
|
},
|
||||||
showEmojiPicker () {
|
showEmojiPicker () {
|
||||||
this.$refs['textarea'].focus()
|
if (!this.activeEmojiInput || !this.activeTextInput)
|
||||||
this.$refs['emoji-input'].triggerShowPicker()
|
this.focusStatusInput()
|
||||||
|
|
||||||
|
this.$refs[this.activeTextInput].focus()
|
||||||
|
this.$refs[this.activeEmojiInput].triggerShowPicker()
|
||||||
|
},
|
||||||
|
focusStatusInput() {
|
||||||
|
this.activeEmojiInput = 'emoji-input'
|
||||||
|
this.activeTextInput = 'textarea'
|
||||||
|
},
|
||||||
|
focusSubjectInput() {
|
||||||
|
this.activeEmojiInput = 'subject-emoji-input'
|
||||||
|
this.activeTextInput = 'subject-input'
|
||||||
|
},
|
||||||
|
toggleSubjectVisible() {
|
||||||
|
// If hiding CW, then we need to clear the subject and reset focus
|
||||||
|
if (this.subjectVisible)
|
||||||
|
{
|
||||||
|
this.focusStatusInput()
|
||||||
|
|
||||||
|
// "nsfw" property is normally set by the @change listener, but this bypasses it.
|
||||||
|
// We need to clear it manually instead.
|
||||||
|
this.newStatus.spoilerText = ''
|
||||||
|
this.newStatus.nsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subjectVisible = !this.subjectVisible
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError () {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|
|
@ -118,13 +118,16 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
v-if="subjectVisible"
|
||||||
|
ref="subject-emoji-input"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
ref="subject-input"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
|
@ -132,6 +135,7 @@
|
||||||
size="1"
|
size="1"
|
||||||
class="form-post-subject"
|
class="form-post-subject"
|
||||||
@input="onSubjectInput"
|
@input="onSubjectInput"
|
||||||
|
@focus="focusSubjectInput()"
|
||||||
>
|
>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
|
@ -166,13 +170,14 @@
|
||||||
cols="1"
|
cols="1"
|
||||||
:disabled="posting && !optimisticPosting"
|
:disabled="posting && !optimisticPosting"
|
||||||
class="form-post-body"
|
class="form-post-body"
|
||||||
:class="{ 'scrollable-form': !!maxHeight }"
|
:class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }"
|
||||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||||
@keydown.meta.enter="postStatus($event, newStatus)"
|
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||||
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||||
@input="resize"
|
@input="resize"
|
||||||
@compositionupdate="resize"
|
@compositionupdate="resize"
|
||||||
@paste="paste"
|
@paste="paste"
|
||||||
|
@focus="focusStatusInput()"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="hasStatusLengthLimit"
|
v-if="hasStatusLengthLimit"
|
||||||
|
@ -185,6 +190,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="!disableScopeSelector"
|
v-if="!disableScopeSelector"
|
||||||
class="visibility-tray"
|
class="visibility-tray"
|
||||||
|
:class="{ 'visibility-tray-edit': isEdit }"
|
||||||
>
|
>
|
||||||
<scope-selector
|
<scope-selector
|
||||||
v-if="!disableVisibilitySelector"
|
v-if="!disableVisibilitySelector"
|
||||||
|
@ -195,7 +201,9 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="language-selector"
|
class="format-selector-container">
|
||||||
|
<div
|
||||||
|
class="format-selector"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
id="post-language"
|
id="post-language"
|
||||||
|
@ -203,17 +211,17 @@
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="language in isoLanguages"
|
v-for="language in postLanguageOptions"
|
||||||
:key="language"
|
:key="language.key"
|
||||||
:value="language"
|
:value="language.value"
|
||||||
>
|
>
|
||||||
{{ language }}
|
{{ language.label }}
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="postFormats.length > 1"
|
v-if="postFormats.length > 1"
|
||||||
class="text-format"
|
class="text-format format-selector"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
id="post-content-type"
|
id="post-content-type"
|
||||||
|
@ -231,7 +239,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
|
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
|
||||||
class="text-format"
|
class="text-format format-selector"
|
||||||
>
|
>
|
||||||
<span class="only-format">
|
<span class="only-format">
|
||||||
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
|
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
|
||||||
|
@ -239,6 +247,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<poll-form
|
<poll-form
|
||||||
v-if="pollsAvailable"
|
v-if="pollsAvailable"
|
||||||
ref="pollForm"
|
ref="pollForm"
|
||||||
|
@ -276,6 +285,15 @@
|
||||||
>
|
>
|
||||||
<FAIcon icon="poll-h" />
|
<FAIcon icon="poll-h" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!disableSubject"
|
||||||
|
class="spoiler-icon button-unstyled"
|
||||||
|
:class="{ selected: subjectVisible }"
|
||||||
|
:title="$t('post_status.toggle_content_warning')"
|
||||||
|
@click="toggleSubjectVisible"
|
||||||
|
>
|
||||||
|
<FAIcon icon="eye-slash" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="posting"
|
v-if="posting"
|
||||||
|
@ -446,6 +464,10 @@
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-tray-edit {
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
.visibility-notice.edit-warning {
|
.visibility-notice.edit-warning {
|
||||||
> :first-child {
|
> :first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -456,7 +478,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-upload-icon, .poll-icon, .emoji-icon {
|
.format-selector-container {
|
||||||
|
.format-selector {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
|
||||||
font-size: 1.85em;
|
font-size: 1.85em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -499,6 +527,11 @@
|
||||||
|
|
||||||
.poll-icon {
|
.poll-icon {
|
||||||
order: 3;
|
order: 3;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-icon {
|
||||||
|
order: 4;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,6 +584,11 @@
|
||||||
line-height: 1.85;
|
line-height: 1.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-post-subject {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-post-body {
|
.form-post-body {
|
||||||
// TODO: make a resizable textarea component?
|
// TODO: make a resizable textarea component?
|
||||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||||
|
@ -563,6 +601,11 @@
|
||||||
min-height: calc(var(--post-line-height) * 1em);
|
min-height: calc(var(--post-line-height) * 1em);
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
||||||
|
&.-has-subject {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.scrollable-form {
|
&.scrollable-form {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
v-if="isLoggedIn && !resettingForm"
|
v-if="isLoggedIn && !resettingForm"
|
||||||
:is-open="modalActivated"
|
:is-open="modalActivated"
|
||||||
class="post-form-modal-view"
|
class="post-form-modal-view"
|
||||||
@backdropClicked="closeModal"
|
@backdrop-clicked="closeModal"
|
||||||
>
|
>
|
||||||
<div class="post-form-modal-panel panel">
|
<div class="post-form-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|
|
@ -8,13 +8,13 @@
|
||||||
remove-padding
|
remove-padding
|
||||||
@show="focusInput"
|
@show="focusInput"
|
||||||
>
|
>
|
||||||
<template v-slot:content="{close}">
|
<template #content="{close}">
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
:enable-sticker-picker="false"
|
:enable-sticker-picker="false"
|
||||||
@emoji="addReaction($event, close)"
|
@emoji="addReaction($event, close)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled popover-trigger"
|
class="button-unstyled popover-trigger"
|
||||||
:title="$t('tool_tip.add_reaction')"
|
:title="$t('tool_tip.add_reaction')"
|
||||||
|
|
|
@ -2,7 +2,7 @@ export default {
|
||||||
props: [ 'user' ],
|
props: [ 'user' ],
|
||||||
computed: {
|
computed: {
|
||||||
subscribeUrl () {
|
subscribeUrl () {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const serverUrl = new URL(this.user.statusnet_profile_url)
|
const serverUrl = new URL(this.user.statusnet_profile_url)
|
||||||
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
|
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,9 @@
|
||||||
|
|
||||||
.interactive {
|
.interactive {
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
animation: unset;
|
||||||
|
}
|
||||||
animation-duration: 0.6s;
|
animation-duration: 0.6s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
:items="items"
|
:items="items"
|
||||||
:get-key="getKey"
|
:get-key="getKey"
|
||||||
>
|
>
|
||||||
<template v-slot:item="{item}">
|
<template #item="{item}">
|
||||||
<div
|
<div
|
||||||
class="selectable-list-item-inner"
|
class="selectable-list-item-inner"
|
||||||
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
|
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:empty>
|
<template #empty>
|
||||||
<slot name="empty" />
|
<slot name="empty" />
|
||||||
</template>
|
</template>
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="state"
|
:model-value="state"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@update:modelValue="update"
|
@update:model-value="update"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
v-if="!!$slots.default"
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<Select
|
<Select
|
||||||
:model-value="state"
|
:model-value="state"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@update:modelValue="update"
|
@update:model-value="update"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
<Popover
|
<Popover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
>
|
>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
|
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="wrench"
|
icon="wrench"
|
||||||
:aria-label="$t('settings.setting_changed')"
|
:aria-label="$t('settings.setting_changed')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="modified-tooltip">
|
<div class="modified-tooltip">
|
||||||
{{ $t('settings.setting_changed') }}
|
{{ $t('settings.setting_changed') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
<Popover
|
<Popover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
>
|
>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
|
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="server"
|
icon="server"
|
||||||
:aria-label="$t('settings.setting_server_side')"
|
:aria-label="$t('settings.setting_server_side')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="serverside-tooltip">
|
<div class="serverside-tooltip">
|
||||||
{{ $t('settings.setting_server_side') }}
|
{{ $t('settings.setting_server_side') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -69,7 +69,7 @@ const SettingsModal = {
|
||||||
this.$store.dispatch('closeSettingsModal')
|
this.$store.dispatch('closeSettingsModal')
|
||||||
},
|
},
|
||||||
logout () {
|
logout () {
|
||||||
this.$router.replace('/main/public')
|
this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all')
|
||||||
this.$store.dispatch('closeSettingsModal')
|
this.$store.dispatch('closeSettingsModal')
|
||||||
this.$store.dispatch('logout')
|
this.$store.dispatch('logout')
|
||||||
},
|
},
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="!!expertLevel"
|
:model-value="!!expertLevel"
|
||||||
class="expertMode"
|
class="expertMode"
|
||||||
@update:modelValue="expertLevel = Number($event)"
|
@update:model-value="expertLevel = Number($event)"
|
||||||
>
|
>
|
||||||
{{ $t("settings.expert_mode") }}
|
{{ $t("settings.expert_mode") }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
|
@ -72,7 +72,7 @@ const DataImportExportTab = {
|
||||||
// check is it's a local user
|
// check is it's a local user
|
||||||
if (user && user.is_local) {
|
if (user && user.is_local) {
|
||||||
// append the instance address
|
// append the instance address
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return user.screen_name + '@' + location.hostname
|
return user.screen_name + '@' + location.hostname
|
||||||
}
|
}
|
||||||
return user.screen_name
|
return user.screen_name
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
|
|
||||||
|
import { usePostLanguageOptions } from 'src/lib/post_language'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -17,6 +18,11 @@ library.add(
|
||||||
)
|
)
|
||||||
|
|
||||||
const GeneralTab = {
|
const GeneralTab = {
|
||||||
|
setup() {
|
||||||
|
const {postLanguageOptions} = usePostLanguageOptions()
|
||||||
|
|
||||||
|
return {postLanguageOptions}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
|
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
|
||||||
|
@ -118,6 +124,12 @@ const GeneralTab = {
|
||||||
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
|
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
postLanguage: {
|
||||||
|
get: function () { return this.$store.getters.mergedConfig.postLanguage },
|
||||||
|
set: function (val) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
|
||||||
|
}
|
||||||
|
},
|
||||||
...SharedComputedObject()
|
...SharedComputedObject()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
<template
|
<template
|
||||||
v-if="profilesExpanded"
|
v-if="profilesExpanded"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="profile in settingsProfiles"
|
v-for="profile in settingsProfiles"
|
||||||
:key="profile.id"
|
:key="profile.id"
|
||||||
|
@ -73,15 +72,24 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn button-default" @click="refreshProfiles()">
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click="refreshProfiles()"
|
||||||
|
>
|
||||||
{{ $t('settings.settings_profiles_refresh') }}
|
{{ $t('settings.settings_profiles_refresh') }}
|
||||||
<FAIcon icon="sync" @click="refreshProfiles()" />
|
<FAIcon
|
||||||
|
icon="sync"
|
||||||
|
@click="refreshProfiles()"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
|
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
|
||||||
<label for="settings-profile-new-name">
|
<label for="settings-profile-new-name">
|
||||||
{{ $t('settings.settings_profile_creation_new_name_label') }}
|
{{ $t('settings.settings_profile_creation_new_name_label') }}
|
||||||
</label>
|
</label>
|
||||||
<input v-model="newProfileName" id="settings-profile-new-name">
|
<input
|
||||||
|
id="settings-profile-new-name"
|
||||||
|
v-model="newProfileName"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
@click="createSettingsProfile"
|
@click="createSettingsProfile"
|
||||||
|
@ -146,6 +154,11 @@
|
||||||
{{ $t('settings.show_wider_shortcuts') }}
|
{{ $t('settings.show_wider_shortcuts') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="displayPageBackgrounds">
|
||||||
|
{{ $t('settings.show_page_backgrounds') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="stopGifs">
|
<BooleanSetting path="stopGifs">
|
||||||
{{ $t('settings.stop_gifs') }}
|
{{ $t('settings.stop_gifs') }}
|
||||||
|
@ -483,14 +496,6 @@
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<li>
|
|
||||||
<BooleanSetting
|
|
||||||
path="useAtIcon"
|
|
||||||
expert="1"
|
|
||||||
>
|
|
||||||
{{ $t('settings.use_at_icon') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="mentionLinkShowAvatar">
|
<BooleanSetting path="mentionLinkShowAvatar">
|
||||||
{{ $t('settings.mention_link_show_avatar') }}
|
{{ $t('settings.mention_link_show_avatar') }}
|
||||||
|
@ -588,6 +593,15 @@
|
||||||
{{ $t('settings.post_status_content_type') }}
|
{{ $t('settings.post_status_content_type') }}
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
id="postLanguage"
|
||||||
|
path="postLanguage"
|
||||||
|
:options="postLanguageOptions"
|
||||||
|
>
|
||||||
|
{{ $t('settings.post_language') }}
|
||||||
|
</ChoiceSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="alwaysShowNewPostButton"
|
path="alwaysShowNewPostButton"
|
||||||
|
|
|
@ -10,17 +10,20 @@ import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
||||||
|
|
||||||
const BlockList = withSubscription({
|
const BlockList = withLoadMore({
|
||||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||||
childPropName: 'items'
|
childPropName: 'items',
|
||||||
|
destroy: () => {}
|
||||||
})(SelectableList)
|
})(SelectableList)
|
||||||
|
|
||||||
const MuteList = withSubscription({
|
const MuteList = withLoadMore({
|
||||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||||
childPropName: 'items'
|
childPropName: 'items',
|
||||||
|
destroy: () => {}
|
||||||
})(SelectableList)
|
})(SelectableList)
|
||||||
|
|
||||||
const DomainMuteList = withSubscription({
|
const DomainMuteList = withSubscription({
|
||||||
|
@ -82,7 +85,7 @@ const MutesAndBlocks = {
|
||||||
// check is it's a local user
|
// check is it's a local user
|
||||||
if (user && user.is_local) {
|
if (user && user.is_local) {
|
||||||
// append the instance address
|
// append the instance address
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return user.screen_name + '@' + location.hostname
|
return user.screen_name + '@' + location.hostname
|
||||||
}
|
}
|
||||||
return user.screen_name
|
return user.screen_name
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
:query="queryUserIds"
|
:query="queryUserIds"
|
||||||
:placeholder="$t('settings.search_user_to_block')"
|
:placeholder="$t('settings.search_user_to_block')"
|
||||||
>
|
>
|
||||||
<template v-slot="row">
|
<template #default="row">
|
||||||
<BlockCard
|
<BlockCard
|
||||||
:user-id="row.item"
|
:user-id="row.item"
|
||||||
/>
|
/>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
:get-key="i => i"
|
:get-key="i => i"
|
||||||
>
|
>
|
||||||
<template v-slot:header="{selected}">
|
<template #header="{selected}">
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-if="selected.length > 0"
|
v-if="selected.length > 0"
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
:click="() => blockUsers(selected)"
|
:click="() => blockUsers(selected)"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.block') }}
|
{{ $t('user_card.block') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('user_card.block_progress') }}
|
{{ $t('user_card.block_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
@ -39,16 +39,16 @@
|
||||||
:click="() => unblockUsers(selected)"
|
:click="() => unblockUsers(selected)"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.unblock') }}
|
{{ $t('user_card.unblock') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('user_card.unblock_progress') }}
|
{{ $t('user_card.unblock_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item="{item}">
|
<template #item="{item}">
|
||||||
<BlockCard :user-id="item" />
|
<BlockCard :user-id="item" />
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:empty>
|
<template #empty>
|
||||||
{{ $t('settings.no_blocks') }}
|
{{ $t('settings.no_blocks') }}
|
||||||
</template>
|
</template>
|
||||||
</BlockList>
|
</BlockList>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
:query="queryUserIds"
|
:query="queryUserIds"
|
||||||
:placeholder="$t('settings.search_user_to_mute')"
|
:placeholder="$t('settings.search_user_to_mute')"
|
||||||
>
|
>
|
||||||
<template v-slot="row">
|
<template #default="row">
|
||||||
<MuteCard
|
<MuteCard
|
||||||
:user-id="row.item"
|
:user-id="row.item"
|
||||||
/>
|
/>
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
:get-key="i => i"
|
:get-key="i => i"
|
||||||
>
|
>
|
||||||
<template v-slot:header="{selected}">
|
<template #header="{selected}">
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-if="selected.length > 0"
|
v-if="selected.length > 0"
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
:click="() => muteUsers(selected)"
|
:click="() => muteUsers(selected)"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.mute') }}
|
{{ $t('user_card.mute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('user_card.mute_progress') }}
|
{{ $t('user_card.mute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
@ -92,16 +92,16 @@
|
||||||
:click="() => unmuteUsers(selected)"
|
:click="() => unmuteUsers(selected)"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.unmute') }}
|
{{ $t('user_card.unmute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('user_card.unmute_progress') }}
|
{{ $t('user_card.unmute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item="{item}">
|
<template #item="{item}">
|
||||||
<MuteCard :user-id="item" />
|
<MuteCard :user-id="item" />
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:empty>
|
<template #empty>
|
||||||
{{ $t('settings.no_mutes') }}
|
{{ $t('settings.no_mutes') }}
|
||||||
</template>
|
</template>
|
||||||
</MuteList>
|
</MuteList>
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
:query="queryKnownDomains"
|
:query="queryKnownDomains"
|
||||||
:placeholder="$t('settings.type_domains_to_mute')"
|
:placeholder="$t('settings.type_domains_to_mute')"
|
||||||
>
|
>
|
||||||
<template v-slot="row">
|
<template #default="row">
|
||||||
<DomainMuteCard
|
<DomainMuteCard
|
||||||
:domain="row.item"
|
:domain="row.item"
|
||||||
/>
|
/>
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
:get-key="i => i"
|
:get-key="i => i"
|
||||||
>
|
>
|
||||||
<template v-slot:header="{selected}">
|
<template #header="{selected}">
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-if="selected.length > 0"
|
v-if="selected.length > 0"
|
||||||
|
@ -133,16 +133,16 @@
|
||||||
:click="() => unmuteDomains(selected)"
|
:click="() => unmuteDomains(selected)"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item="{item}">
|
<template #item="{item}">
|
||||||
<DomainMuteCard :domain="item" />
|
<DomainMuteCard :domain="item" />
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:empty>
|
<template #empty>
|
||||||
{{ $t('settings.no_mutes') }}
|
{{ $t('settings.no_mutes') }}
|
||||||
</template>
|
</template>
|
||||||
</DomainMuteList>
|
</DomainMuteList>
|
||||||
|
|
|
@ -33,6 +33,7 @@ const ProfileTab = {
|
||||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
|
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
|
||||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
role: this.$store.state.users.currentUser.role,
|
role: this.$store.state.users.currentUser.role,
|
||||||
|
@ -129,14 +130,15 @@ const ProfileTab = {
|
||||||
note: this.newBio,
|
note: this.newBio,
|
||||||
locked: this.newLocked,
|
locked: this.newLocked,
|
||||||
// Backend notation.
|
// Backend notation.
|
||||||
/* eslint-disable camelcase */
|
|
||||||
display_name: this.newName,
|
display_name: this.newName,
|
||||||
fields_attributes: this.newFields.filter(el => el != null),
|
fields_attributes: this.newFields.filter(el => el != null),
|
||||||
bot: this.bot,
|
bot: this.bot,
|
||||||
show_role: this.showRole,
|
show_role: this.showRole,
|
||||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
||||||
|
permit_followback: this.permit_followback,
|
||||||
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
||||||
/* eslint-enable camelcase */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.emailLanguage) {
|
if (this.emailLanguage) {
|
||||||
|
@ -185,7 +187,7 @@ const ProfileTab = {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = ({ target }) => {
|
reader.onload = ({ target }) => {
|
||||||
const img = target.result
|
const img = target.result
|
||||||
|
|
|
@ -110,11 +110,9 @@
|
||||||
max="730"
|
max="730"
|
||||||
class="expire-posts-days"
|
class="expire-posts-days"
|
||||||
:placeholder="$t('settings.expire_posts_input_placeholder')"
|
:placeholder="$t('settings.expire_posts_input_placeholder')"
|
||||||
/>
|
>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
<p />
|
||||||
<p>
|
<p>
|
||||||
<interface-language-switcher
|
<interface-language-switcher
|
||||||
:prompt-text="$t('settings.email_language')"
|
:prompt-text="$t('settings.email_language')"
|
||||||
|
@ -259,6 +257,19 @@
|
||||||
<BooleanSetting path="serverSide_locked">
|
<BooleanSetting path="serverSide_locked">
|
||||||
{{ $t('settings.lock_account_description') }}
|
{{ $t('settings.lock_account_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
<ul
|
||||||
|
class="setting-list suboptions"
|
||||||
|
:class="[{disabled: !serverSide_locked}]"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="serverSide_permitFollowback"
|
||||||
|
:disabled="!serverSide_locked"
|
||||||
|
>
|
||||||
|
{{ $t('settings.permit_followback_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_discoverable">
|
<BooleanSetting path="serverSide_discoverable">
|
||||||
|
|
|
@ -268,6 +268,10 @@
|
||||||
.side-drawer {
|
.side-drawer {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition-timing-function: unset;
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
transition: 0.35s;
|
transition: 0.35s;
|
||||||
transition-property: transform;
|
transition-property: transform;
|
||||||
margin: 0 0 0 -100px;
|
margin: 0 0 0 -100px;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import { muteWordHits } from '../../services/status_parser/status_parser.js'
|
import { muteWordHits } from '../../services/status_parser/status_parser.js'
|
||||||
import { unescape, uniqBy } from 'lodash'
|
import { unescape, uniqBy } from 'lodash'
|
||||||
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -117,7 +118,8 @@ const Status = {
|
||||||
RichContent,
|
RichContent,
|
||||||
MentionLink,
|
MentionLink,
|
||||||
MentionsLine,
|
MentionsLine,
|
||||||
QuoteButton
|
QuoteButton,
|
||||||
|
StillImage
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'statusoid',
|
'statusoid',
|
||||||
|
|
|
@ -266,6 +266,16 @@
|
||||||
color: $fallback--cGreen;
|
color: $fallback--cGreen;
|
||||||
color: var(--cGreen, $fallback--cGreen);
|
color: var(--cGreen, $fallback--cGreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeat-tooltip {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeater-avatar {
|
.repeater-avatar {
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
:user="statusoid.user"
|
:user="statusoid.user"
|
||||||
/>
|
/>
|
||||||
<div class="right-side faint">
|
<div class="right-side faint">
|
||||||
<span
|
<div
|
||||||
class="status-username repeater-name"
|
class="status-username repeater-name"
|
||||||
:title="retweeter"
|
:title="retweeter"
|
||||||
>
|
>
|
||||||
|
@ -100,8 +100,12 @@
|
||||||
v-else
|
v-else
|
||||||
:to="retweeterProfileLink"
|
:to="retweeterProfileLink"
|
||||||
>{{ retweeter }}</router-link>
|
>{{ retweeter }}</router-link>
|
||||||
</span>
|
</div>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="repeat-tooltip"
|
||||||
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="retweet"
|
icon="retweet"
|
||||||
class="repeat-icon"
|
class="repeat-icon"
|
||||||
|
@ -110,6 +114,7 @@
|
||||||
{{ $t('timeline.repeated') }}
|
{{ $t('timeline.repeated') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!deleted"
|
v-if="!deleted"
|
||||||
|
@ -174,12 +179,12 @@
|
||||||
>
|
>
|
||||||
@{{ status.user.screen_name_ui }}
|
@{{ status.user.screen_name_ui }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<img
|
<StillImage
|
||||||
v-if="!!(status.user && status.user.favicon)"
|
v-if="!!(status.user && status.user.favicon)"
|
||||||
class="status-favicon"
|
class="status-favicon"
|
||||||
:src="status.user.favicon"
|
:src="status.user.favicon"
|
||||||
:title="faviconAlt(status)"
|
:title="faviconAlt(status)"
|
||||||
>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -190,7 +195,7 @@
|
||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="status.created_at"
|
:time="status.created_at"
|
||||||
:with-direction="true"
|
:with-direction="!compact"
|
||||||
:auto-update="60"
|
:auto-update="60"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -368,7 +373,7 @@
|
||||||
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||||
@mediaplay="addMediaPlaying($event)"
|
@mediaplay="addMediaPlaying($event)"
|
||||||
@mediapause="removeMediaPlaying($event)"
|
@mediapause="removeMediaPlaying($event)"
|
||||||
@parseReady="setHeadTailLinks"
|
@parse-ready="setHeadTailLinks"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -476,8 +481,8 @@
|
||||||
/>
|
/>
|
||||||
<extra-buttons
|
<extra-buttons
|
||||||
:status="status"
|
:status="status"
|
||||||
@onError="showError"
|
@on-error="showError"
|
||||||
@onSuccess="clearError"
|
@on-success="clearError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
|
|
||||||
.emoji:hover {
|
.emoji:hover {
|
||||||
transform: scale(1.4);
|
transform: scale(1.4);
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
transition: 0.05s;
|
transition: 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
||||||
:greentext="mergedConfig.greentext"
|
:greentext="mergedConfig.greentext"
|
||||||
:attentions="status.attentions"
|
:attentions="status.attentions"
|
||||||
@parseReady="onParseReady"
|
@parse-ready="onParseReady"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="status.translation"
|
v-if="status.translation"
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
||||||
:greentext="mergedConfig.greentext"
|
:greentext="mergedConfig.greentext"
|
||||||
:attentions="status.attentions"
|
:attentions="status.attentions"
|
||||||
@parseReady="onParseReady"
|
@parse-ready="onParseReady"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label class="label">{{ $t('status.override_translation_source_language') }}</label>
|
<label class="label">{{ $t('status.override_translation_source_language') }}</label>
|
||||||
|
@ -89,7 +89,10 @@
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<button @click="translateStatus" class="btn button-default">
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click="translateStatus"
|
||||||
|
>
|
||||||
{{ $t('status.translate') }}
|
{{ $t('status.translate') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
:toggle-showing-tall="toggleShowingTall"
|
:toggle-showing-tall="toggleShowingTall"
|
||||||
:toggle-expanding-subject="toggleExpandingSubject"
|
:toggle-expanding-subject="toggleExpandingSubject"
|
||||||
:toggle-showing-long-subject="toggleShowingLongSubject"
|
:toggle-showing-long-subject="toggleShowingLongSubject"
|
||||||
@parseReady="$emit('parseReady', $event)"
|
@parse-ready="$emit('parseReady', $event)"
|
||||||
>
|
>
|
||||||
<div v-if="status.poll && status.poll.options && !compact">
|
<div v-if="status.poll && status.poll.options && !compact">
|
||||||
<Poll
|
<Poll
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<Modal
|
<Modal
|
||||||
v-if="modalActivated"
|
v-if="modalActivated"
|
||||||
class="status-history-modal-view"
|
class="status-history-modal-view"
|
||||||
@backdropClicked="closeModal"
|
@backdrop-clicked="closeModal"
|
||||||
>
|
>
|
||||||
<div class="status-history-modal-panel panel">
|
<div class="status-history-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
v-for="status in history"
|
v-for="status in history"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:isPreview="true"
|
:is-preview="true"
|
||||||
class="conversation-status status-fadein panel-body"
|
class="conversation-status status-fadein panel-body"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
@show="enter"
|
@show="enter"
|
||||||
>
|
>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<Status
|
<Status
|
||||||
v-if="status"
|
v-if="status"
|
||||||
:is-preview="true"
|
:is-preview="true"
|
||||||
|
|
|
@ -11,8 +11,9 @@ const StillImage = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
stopGifs: this.$store.getters.mergedConfig.stopGifs,
|
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
isAnimated: false
|
isAnimated: false,
|
||||||
|
imageTypeLabel: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -39,14 +40,24 @@ const StillImage = {
|
||||||
this.imageLoadError && this.imageLoadError()
|
this.imageLoadError && this.imageLoadError()
|
||||||
},
|
},
|
||||||
detectAnimation (image) {
|
detectAnimation (image) {
|
||||||
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
|
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
||||||
|
|
||||||
|
if (!mediaProxyAvailable) {
|
||||||
|
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
|
||||||
|
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
|
||||||
|
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
// Since the canvas and images are not pixel-perfect matching (due to scaling),
|
||||||
|
// It makes the images jiggle on hover, which is not ideal for accessibility, methinks
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// harmless CORS errors without-- clean console with
|
this.detectWithoutMediaProxy(image)
|
||||||
if (!this.$store.state.instance.mediaProxyAvailable) return
|
} else {
|
||||||
// Animated JPEGs?
|
this.detectWithMediaProxy(image)
|
||||||
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
|
}
|
||||||
|
},
|
||||||
|
detectAnimationWithFetch (image) {
|
||||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||||
fetch(image.src, {
|
fetch(image.src, {
|
||||||
referrerPolicy: 'same-origin'
|
referrerPolicy: 'same-origin'
|
||||||
|
@ -55,12 +66,20 @@ const StillImage = {
|
||||||
// We don't need to read the whole file so only call it once
|
// We don't need to read the whole file so only call it once
|
||||||
data.body.getReader().read()
|
data.body.getReader().read()
|
||||||
.then(reader => {
|
.then(reader => {
|
||||||
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
|
// Ordered from least to most intensive
|
||||||
|
if (this.isGIF(reader.value)) {
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
|
this.setLabel('GIF')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
|
if (this.isAnimatedWEBP(reader.value)) {
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
|
this.setLabel('WEBP')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isAnimatedPNG(reader.value)) {
|
||||||
|
this.isAnimated = true
|
||||||
|
this.setLabel('APNG')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -68,6 +87,53 @@ const StillImage = {
|
||||||
// this.imageLoadError && this.imageLoadError()
|
// this.imageLoadError && this.imageLoadError()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
detectWithMediaProxy (image) {
|
||||||
|
this.detectAnimationWithFetch(image)
|
||||||
|
},
|
||||||
|
detectWithoutMediaProxy (image) {
|
||||||
|
// We'll just assume that gifs and webp are animated
|
||||||
|
const extension = image.src.split('.').pop().toLowerCase()
|
||||||
|
|
||||||
|
if (extension === 'gif') {
|
||||||
|
this.isAnimated = true
|
||||||
|
this.setLabel('GIF')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (extension === 'webp') {
|
||||||
|
this.isAnimated = true
|
||||||
|
this.setLabel('WEBP')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Beware the apng! use this if ye dare
|
||||||
|
// if (extension === 'png') {
|
||||||
|
// this.isAnimated = true
|
||||||
|
// this.setLabel('PNG')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Hail mary for extensionless
|
||||||
|
if (extension.includes('/')) {
|
||||||
|
// Don't mind the CORS error barrage
|
||||||
|
this.detectAnimationWithFetch(image)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLabel (name) {
|
||||||
|
this.imageTypeLabel = name;
|
||||||
|
},
|
||||||
|
isGIF (data) {
|
||||||
|
// I am a perfectly sane individual
|
||||||
|
//
|
||||||
|
// GIF HEADER CHUNK
|
||||||
|
// === START HEADER ===
|
||||||
|
// 47 49 46 38 ("GIF8")
|
||||||
|
const gifHeader = [0x47, 0x49, 0x46];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (data[i] !== gifHeader[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
isAnimatedWEBP (data) {
|
isAnimatedWEBP (data) {
|
||||||
/**
|
/**
|
||||||
* WEBP HEADER CHUNK
|
* WEBP HEADER CHUNK
|
||||||
|
@ -102,14 +168,53 @@ const StillImage = {
|
||||||
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
||||||
},
|
},
|
||||||
drawThumbnail() {
|
drawThumbnail() {
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas;
|
||||||
if (!this.$refs.canvas) return
|
if (!canvas) return;
|
||||||
const image = this.$refs.src
|
|
||||||
const width = image.naturalWidth
|
const context = canvas.getContext('2d');
|
||||||
const height = image.naturalHeight
|
const image = this.$refs.src;
|
||||||
canvas.width = width
|
const parentElement = canvas.parentElement;
|
||||||
canvas.height = height
|
|
||||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
// Draw the quick, unscaled version first
|
||||||
|
context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to schedule the scaling to the next frame
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Compute scaling ratio between the natural dimensions of the image and its display dimensions
|
||||||
|
const scalingRatioWidth = parentElement.clientWidth / image.naturalWidth;
|
||||||
|
const scalingRatioHeight = parentElement.clientHeight / image.naturalHeight;
|
||||||
|
|
||||||
|
// Adjust for high-DPI displays
|
||||||
|
const ratio = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = image.naturalWidth * scalingRatioWidth * ratio;
|
||||||
|
canvas.height = image.naturalHeight * scalingRatioHeight * ratio;
|
||||||
|
canvas.style.width = `${parentElement.clientWidth}px`;
|
||||||
|
canvas.style.height = `${parentElement.clientHeight}px`;
|
||||||
|
context.scale(ratio, ratio);
|
||||||
|
|
||||||
|
// Maintain the aspect ratio of the image
|
||||||
|
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
|
||||||
|
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
|
||||||
|
|
||||||
|
let drawWidth, drawHeight;
|
||||||
|
|
||||||
|
if (imgAspectRatio > canvasAspectRatio) {
|
||||||
|
drawWidth = parentElement.clientWidth;
|
||||||
|
drawHeight = parentElement.clientWidth / imgAspectRatio;
|
||||||
|
} else {
|
||||||
|
drawHeight = parentElement.clientHeight;
|
||||||
|
drawWidth = parentElement.clientHeight * imgAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
// Draw the good one for realsies
|
||||||
|
const dx = (parentElement.clientWidth - drawWidth) / 2;
|
||||||
|
const dy = (parentElement.clientHeight - drawHeight) / 2;
|
||||||
|
context.drawImage(image, dx, dy, drawWidth, drawHeight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated () {
|
updated () {
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="still-image"
|
||||||
class="still-image"
|
class="still-image"
|
||||||
:class="{ animated: animated }"
|
:class="{ animated: animated }"
|
||||||
:style="style"
|
:style="style"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="animated && imageTypeLabel"
|
||||||
|
class="image-type-label"
|
||||||
|
>
|
||||||
|
{{ imageTypeLabel }}
|
||||||
|
</div>
|
||||||
<canvas
|
<canvas
|
||||||
v-if="animated"
|
v-if="animated"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
@ -57,30 +64,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animated {
|
.image-type-label {
|
||||||
&::before {
|
|
||||||
zoom: var(--_still_image-label-scale, 1);
|
|
||||||
content: 'gif';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0.25em;
|
||||||
|
left: 0.25em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 0.7em;
|
font-size: 0.6em;
|
||||||
top: 0.5em;
|
|
||||||
left: 0.5em;
|
|
||||||
background: rgba(127, 127, 127, 0.5);
|
background: rgba(127, 127, 127, 0.5);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: block;
|
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
visibility: var(--_still-image-label-visibility, visible);
|
visibility: var(--_still-image-label-visibility, visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.animated {
|
||||||
&:hover canvas {
|
&:hover canvas {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover::before {
|
&:hover .image-type-label {
|
||||||
visibility: var(--_still-image-label-visibility, hidden);
|
visibility: var(--_still-image-label-visibility, hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
:dive="dive ? () => dive(status.id) : undefined"
|
:dive="dive ? () => dive(status.id) : undefined"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="currentReplies.length && threadShowing"
|
v-if="currentReplies.length && threadShowing"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue