mirror of
https://akkoma.dev/lamp/akkoma-fe.git
synced 2026-06-04 22:30:04 -04:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18be86a8db | |||
| 589d8ea29f | |||
| 52b27396b9 | |||
| c2db0e66ef | |||
| 9f8eba3464 | |||
| 762676e105 | |||
| d6e8d785c8 | |||
| 1fa242232e | |||
| 539977de9d | |||
| d2995ada16 | |||
| bcd15ef858 | |||
| 900ac68ca6 | |||
| 34bbcef83e | |||
| 37ce8352a9 | |||
| f48138c979 | |||
| 5baa2ce40f | |||
| fef531b8a0 | |||
| bf0c137057 | |||
| 5a50ceb3aa | |||
| f08a961199 | |||
| d252e10543 | |||
| 7c84854b10 | |||
| ab606c6160 | |||
| 38d8a9751a | |||
| 2c92467dcd | |||
| bb71635d12 | |||
| e1b4d8f59a | |||
| 2455bb70f3 | |||
| fbc6cd59bc | |||
| 873048de2e | |||
| 7a4e2a8644 | |||
| a1d92ffd86 | |||
| b60f42b959 | |||
| 55dff3a9bd | |||
| 9ef8effeed | |||
| 9c15db16a6 | |||
| 674a816453 | |||
| be2207fa42 | |||
| 3f3ea32f81 | |||
| abc6b299e0 | |||
| b8b18c67b1 | |||
| 4cf4b5e2d0 | |||
| d617a9596a | |||
| 4734e9668d | |||
| 9787f43343 | |||
| 61bdedc82f | |||
| a4eddc7f1c | |||
| 94c5998593 | |||
| 851dd263c0 | |||
| 473ba89355 | |||
| 4ce8ffcec1 | |||
| e62b154228 | |||
| e87a9ced61 | |||
| 7245775b27 | |||
| 6373c5a05d | |||
| cfbf3ecb6d | |||
| 2914eaf1ca | |||
| 578ef52df6 | |||
| 0bf9cb0660 | |||
| 65cb3b95e0 | |||
| f15b94d566 | |||
| 06ba190e2e | |||
| 2086522d64 | |||
| cb63cc38c1 | |||
| fa294e0003 | |||
| d3fa5cfad0 | |||
| 9552287442 | |||
| 6b7c8f0def | |||
| 3386692e26 | |||
| ad6bb47003 | |||
| 9838545904 | |||
| 868c6e41ac | |||
| b3f25e5d84 | |||
| 248509073e | |||
| a7d6235131 | |||
| 177d96f977 | |||
| 42ba77ebf4 | |||
| 4a50b1273d | |||
| c76dc6d79e | |||
| cb4c581cde | |||
| 8231c8f0b6 | |||
| ef242a1ddd | |||
| 35cf3327c8 | |||
| 1ae09458c6 | |||
| f391cf70a4 | |||
| fa8fde2ab1 | |||
| 1f2c96a485 | |||
| 25681cf5f6 | |||
| 6c178aa257 | |||
| 6666a273a4 | |||
| 3210873d7f | |||
| f5f9949253 | |||
| ba4ae5badb | |||
| 56a59e1b55 | |||
| 3065416c93 | |||
| 94141dcb3c | |||
| 92e278d406 | |||
| 3349fe6ff2 | |||
| 94ed0991bc | |||
| e274adf47d | |||
| e955eb4503 | |||
| c39d9fa64b | |||
| a74a631793 | |||
| 2e83ccefdc | |||
| cf11b2523e | |||
| 8765491399 | |||
| 85001814a2 | |||
| c902219997 | |||
| 2e2e87db75 | |||
| b2af067fd3 | |||
| 4211e05a75 | |||
| a3251a1ba6 | |||
| e5608f4009 | |||
| 1092d43802 | |||
| 98a3622172 | |||
| 24b9e350e2 | |||
| 7ab4d22a4c | |||
| 8489f6d5ae | |||
| 754cd2fa57 | |||
| 31055fb4f2 | |||
| 918b0e3770 | |||
| 88aae1706a | |||
| 3d2a8a3ca2 | |||
| a24fff5d5b | |||
| 4abddf5e6a | |||
| 1b4df9e79d | |||
| 45fe334cd7 | |||
| dd32a33d59 | |||
| 74b651a3a2 | |||
| 21fe7d76d3 | |||
| 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 | |||
| 4d91a7b2c3 | |||
| 45524552a0 | |||
| ee66b69ab5 | |||
| d42e374704 | |||
| ce8a9d2b4a | |||
| d2b7ac6d8c | |||
| 754c72cb24 | |||
| f5bd195422 | |||
| d49fd46554 | |||
| 9982373853 | |||
| 5206b5cf9c | |||
| a65a06ca04 | |||
| c10b38afbc | |||
| 009941ea2c | |||
| 042e8c78dc | |||
| 0e07d88afa | |||
| 1f5f8665c8 | |||
| 428ed70b0d | |||
| 7cc6c35654 | |||
| 228679e49e | |||
| d610a46c32 | |||
| ed0b403c33 | |||
| 0925763267 | |||
| 0f842b300b | |||
| e292af4211 | |||
| 865cb6f96a | |||
| 4e7d5d3a08 | |||
| f2d0c4c7d5 | |||
| 02a6591f20 | |||
| 94c70f8914 | |||
| 3ba8c90e1e | |||
| 83db80f88c | |||
| 1489d92997 | |||
| db5c9572dc | |||
| 5bb53c8b0d | |||
| 665f88f5c7 | |||
| 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 | |||
| e7a558a533 | |||
| 174f98b1cb |
@@ -1,2 +0,0 @@
|
||||
build/*.js
|
||||
config/*.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
|
||||
@@ -0,0 +1 @@
|
||||
nodejs 20.12.2
|
||||
+25
-18
@@ -1,19 +1,21 @@
|
||||
pipeline:
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
steps:
|
||||
lint:
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
#- yarn stylelint
|
||||
|
||||
test:
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- apt update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
@@ -27,7 +29,7 @@ pipeline:
|
||||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn build
|
||||
@@ -39,15 +41,18 @@ pipeline:
|
||||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
image: node:20
|
||||
environment:
|
||||
SCW_ACCESS_KEY:
|
||||
from_secret: SCW_ACCESS_KEY
|
||||
SCW_SECRET_KEY:
|
||||
from_secret: SCW_SECRET_KEY
|
||||
SCW_DEFAULT_ORGANIZATION_ID:
|
||||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- zip akkoma-fe.zip -r dist
|
||||
@@ -62,15 +67,17 @@ pipeline:
|
||||
- stable
|
||||
environment:
|
||||
CI: "true"
|
||||
SCW_ACCESS_KEY:
|
||||
from_secret: SCW_ACCESS_KEY
|
||||
SCW_SECRET_KEY:
|
||||
from_secret: SCW_SECRET_KEY
|
||||
SCW_DEFAULT_ORGANIZATION_ID:
|
||||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
image: python:3.10-slim
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget git zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- cd docs
|
||||
|
||||
@@ -20,9 +20,11 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
|
||||
|
||||
## 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
|
||||
# install dependencies
|
||||
npm install -g yarn
|
||||
corepack enable
|
||||
yarn
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
@@ -37,7 +39,7 @@ npm run unit
|
||||
|
||||
# For Contributors:
|
||||
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
||||
You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/config/local.example.json)) to enable some convenience dev options:
|
||||
|
||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||
|
||||
+25
-25
@@ -1,36 +1,36 @@
|
||||
// https://github.com/shelljs/shelljs
|
||||
require('./check-versions')()
|
||||
require('shelljs/global')
|
||||
env.NODE_ENV = 'production'
|
||||
require("./check-versions")();
|
||||
require("shelljs/global");
|
||||
env.NODE_ENV = "production";
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var ora = require('ora')
|
||||
var webpack = require('webpack')
|
||||
var webpackConfig = require('./webpack.prod.conf')
|
||||
var path = require("path");
|
||||
var config = require("../config");
|
||||
var webpack = require("webpack");
|
||||
var webpackConfig = require("./webpack.prod.conf");
|
||||
|
||||
console.log(
|
||||
' Tip:\n' +
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
)
|
||||
" Tip:\n" +
|
||||
" Built files are meant to be served over an HTTP server.\n" +
|
||||
" Opening index.html over file:// won't work.\n",
|
||||
);
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
var assetsPath = path.join(
|
||||
config.build.assetsRoot,
|
||||
config.build.assetsSubDirectory,
|
||||
);
|
||||
rm("-rf", assetsPath);
|
||||
mkdir("-p", assetsPath);
|
||||
cp("-R", "static/*", assetsPath);
|
||||
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
if (err) throw err;
|
||||
process.stdout.write(
|
||||
stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
})
|
||||
chunkModules: false,
|
||||
}) + "\n",
|
||||
);
|
||||
});
|
||||
|
||||
+8
-2
@@ -5,7 +5,7 @@ var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var opn = require('opn')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
@@ -36,7 +36,13 @@ Object.keys(proxyTable).forEach(function (context) {
|
||||
if (typeof options === 'string') {
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@ var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
@@ -35,6 +36,7 @@ module.exports = {
|
||||
],
|
||||
fallback: {
|
||||
"url": require.resolve("url/"),
|
||||
querystring: require.resolve("querystring-es3")
|
||||
},
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
@@ -47,20 +49,6 @@ module.exports = {
|
||||
module: {
|
||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.(js|vue)$/,
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
enforce: 'post',
|
||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
@@ -118,6 +106,9 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,5 +2,4 @@ var { merge } = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
||||
|
||||
@@ -15,12 +15,13 @@ put a file that looks like this
|
||||
|
||||
```json
|
||||
{
|
||||
"myPack": "/static/stickers/myPack"
|
||||
"myPack": "/static/stickers/myPack/"
|
||||
}
|
||||
```
|
||||
|
||||
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
|
||||
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
|
||||
it inside `/static/stickers/myPack`". You can add as many packs as you like in this manner.
|
||||
Note that a single leading and a trailing slash are **required** to work correctly!
|
||||
|
||||
## Creating the pack
|
||||
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -6,7 +6,6 @@
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||
<link rel="stylesheet" href="/static/mfm.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||
<!--server-generated-meta-->
|
||||
|
||||
+81
-83
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.10.0",
|
||||
"version": "3.12.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
@@ -12,120 +12,118 @@
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "stylelint src/**/*.scss",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
"lint": "eslint src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@chenfengyuan/vue-qrcode": "^2.0.0",
|
||||
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"blurhash": "^2.0.4",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"blurhash": "^2.0.5",
|
||||
"body-scroll-lock": "^3.1.5",
|
||||
"chromatism": "^3.0.0",
|
||||
"click-outside-vue3": "^4.0.1",
|
||||
"cropperjs": "^1.6.2",
|
||||
"diff": "^5.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"js-cookie": "^3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"localforage": "^1.10.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
"qrcode": "1",
|
||||
"url": "^0.11.0",
|
||||
"vue": "^3.2.31",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "4.0.14",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuex": "4.0.2"
|
||||
"phoenix": "^1.7.12",
|
||||
"punycode.js": "^2.3.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"querystring-es3": "^0.2.1",
|
||||
"url": "^0.11.3",
|
||||
"vue": "^3.4.38",
|
||||
"vue-i18n": "^9.14.0",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-template-compiler": "^2.7.16",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/core": "^7.24.6",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-transform-runtime": "7.17.0",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/register": "7.17.7",
|
||||
"@babel/plugin-transform-runtime": "^7.24.6",
|
||||
"@babel/preset-env": "^7.24.6",
|
||||
"@babel/register": "^7.24.6",
|
||||
"@intlify/vue-i18n-loader": "^5.0.0",
|
||||
"@ungap/event-target": "0.2.3",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@ungap/event-target": "^0.2.4",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
|
||||
"@vue/babel-plugin-jsx": "^1.2.2",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"autoprefixer": "6.7.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "^107.0.3",
|
||||
"chalk": "^1.1.3",
|
||||
"chromedriver": "^119.0.1",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"css-loader": "^6.7.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint": "^9.3.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"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-vue": "^9.7.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.17.3",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"eslint-webpack-plugin": "^4.2.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.19.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"function-bind": "1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"isparta-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.3.17",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-firefox-launcher": "1.3.0",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"http-proxy-middleware": "^3.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "^6.4.3",
|
||||
"karma-coverage": "^2.2.1",
|
||||
"karma-firefox-launcher": "^2.1.3",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-sinon-chai": "^2.0.2",
|
||||
"karma-sourcemap-loader": "^0.4.0",
|
||||
"karma-spec-reporter": "^0.0.36",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lolex": "1.6.0",
|
||||
"mini-css-extract-plugin": "0.12.0",
|
||||
"mocha": "3.5.3",
|
||||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lolex": "^6.0.0",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"mocha": "^10.4.0",
|
||||
"nightwatch": "^3.6.3",
|
||||
"opn": "^6.0.0",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.7.1",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.77.2",
|
||||
"sass-loader": "^14.2.1",
|
||||
"selenium-server": "^3.141.59",
|
||||
"semver": "^7.6.2",
|
||||
"shelljs": "^0.8.5",
|
||||
"sinon": "^18.0.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"stylelint": "^14.15.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-rscss": "^0.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-dev-middleware": "^5.3.3",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
"vue-loader": "^17.4.2",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-dev-middleware": "^7.2.1",
|
||||
"webpack-hot-middleware": "^2.26.1",
|
||||
"webpack-merge": "^5.10.0",
|
||||
"workbox-webpack-plugin": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0",
|
||||
|
||||
+11
-2
@@ -59,11 +59,17 @@ export default {
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
'-has-new-post-button': this.newPostButtonShown,
|
||||
'-wide-timeline': this.widenTimeline
|
||||
},
|
||||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
pageBackground () {
|
||||
return this.mergedConfig.displayPageBackgrounds
|
||||
? this.$store.state.users.displayBackground
|
||||
: null
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
@@ -71,7 +77,7 @@ export default {
|
||||
? null
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
@@ -88,6 +94,9 @@ export default {
|
||||
newPostButtonShown () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
widenTimeline () {
|
||||
return this.$store.getters.mergedConfig.widenTimeline
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
|
||||
+10
-1
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
||||
}
|
||||
|
||||
@@ -172,6 +172,10 @@ nav {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
|
||||
z-index: -1000;
|
||||
|
||||
.-wide-timeline & {
|
||||
margin:0 calc(var(--columnGap) / -2);
|
||||
}
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
@@ -187,12 +191,17 @@ nav {
|
||||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 0 calc(var(--columnGap) / 2);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
|
||||
&.-wide-timeline {
|
||||
--maxiColumn: minmax(var(--miniColumn), 1fr);
|
||||
}
|
||||
|
||||
.column {
|
||||
--___columnMargin: var(--columnGap);
|
||||
|
||||
|
||||
@@ -183,6 +183,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||
copyInstanceOption('renderMisskeyMarkdown')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
if (config.backendCommitUrl)
|
||||
copyInstanceOption('backendCommitUrl')
|
||||
|
||||
if (config.frontendCommitUrl)
|
||||
copyInstanceOption('frontendCommitUrl')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
@@ -71,7 +71,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
@@ -93,7 +93,7 @@
|
||||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template v-slot:user>
|
||||
<template #user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
|
||||
.attachment-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
align-content: center;
|
||||
|
||||
.status-popover & {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.description-container {
|
||||
@@ -37,7 +42,7 @@
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
@@ -115,6 +120,22 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0.5em;
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
padding: 0 0.5em;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--border) #0000;
|
||||
|
||||
.status-popover & {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
height: 1lh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -246,8 +246,8 @@
|
||||
ref="flash"
|
||||
class="flash"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
@playerOpened="setFlashLoaded(true)"
|
||||
@playerClosed="setFlashLoaded(false)"
|
||||
@player-opened="setFlashLoaded(true)"
|
||||
@player-closed="setFlashLoaded(false)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
],
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
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">
|
||||
<input
|
||||
@@ -46,7 +46,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
@@ -108,6 +107,7 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
</dialog-modal>
|
||||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../_variables';
|
||||
|
||||
@@ -35,5 +37,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
@@ -267,11 +267,11 @@ const conversation = {
|
||||
},
|
||||
replies () {
|
||||
let i = 1
|
||||
// eslint-disable-next-line camelcase
|
||||
|
||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
@@ -414,6 +414,14 @@ const conversation = {
|
||||
},
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
const navHeight = document.getElementById("nav").offsetHeight
|
||||
const headingHeight = document.getElementsByClassName("timeline-heading")[0].offsetHeight
|
||||
document.documentElement.style.setProperty("--timeline-scroll-margin-top", `${navHeight + headingHeight}px`)
|
||||
this.$nextTick(() => {
|
||||
if (!this.expanded) {
|
||||
this.$el.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
})
|
||||
},
|
||||
getConversationId (statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
@@ -184,7 +184,7 @@
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,5 +278,7 @@
|
||||
&.-expanded.status-fadein {
|
||||
margin: calc(var(--status-margin, $status-margin) / 2);
|
||||
}
|
||||
|
||||
scroll-margin-block-start: var(--timeline-scroll-margin-top);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="publicTimelineVisible"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="publicTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
@@ -68,9 +68,9 @@
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federatedTimelineVisible"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="federatedTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
@@ -19,7 +19,7 @@
|
||||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Modal
|
||||
v-if="isFormVisible"
|
||||
class="edit-form-modal-view"
|
||||
@backdropClicked="closeModal"
|
||||
@backdrop-clicked="closeModal"
|
||||
>
|
||||
<div class="edit-form-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
@@ -11,10 +11,10 @@
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
:post-handler="doEditStatus"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
const EMOJI_SIZE = 32 + 8
|
||||
const GROUP_TITLE_HEIGHT = 24
|
||||
const BUFFER_SIZE = 3 * EMOJI_SIZE
|
||||
@@ -17,6 +19,9 @@ const EmojiGrid = {
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StillImage
|
||||
},
|
||||
mounted () {
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
this.containerWidth = rect.width
|
||||
|
||||
@@ -34,10 +34,11 @@
|
||||
@click.stop.prevent="onEmoji(item.emoji)"
|
||||
>
|
||||
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
|
||||
<img
|
||||
<StillImage
|
||||
v-else
|
||||
:src="item.emoji.imageUrl"
|
||||
>
|
||||
noStopGifs="true"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Completion from '../../services/completion/completion.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
@@ -120,7 +121,8 @@ const EmojiInput = {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
StillImage
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
ref="picker"
|
||||
show-keep-open
|
||||
:class="{ hide: !showPicker }"
|
||||
:visible="showPicker"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="insert"
|
||||
@@ -43,11 +44,15 @@
|
||||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<img
|
||||
<span
|
||||
v-if="!suggestion.mfm"
|
||||
class="image"
|
||||
>
|
||||
<StillImage
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
noStopGifs="true"
|
||||
/>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
|
||||
/**
|
||||
@@ -122,14 +122,14 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
||||
suggestions = newSuggestions || []
|
||||
return suggestions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBoxOpen,
|
||||
@@ -26,12 +27,17 @@ const EmojiPicker = {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
visible: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
activeGroup: this.getDefaultGroup(),
|
||||
showingStickers: false,
|
||||
keepOpen: false
|
||||
}
|
||||
@@ -39,7 +45,8 @@ const EmojiPicker = {
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox,
|
||||
EmojiGrid
|
||||
EmojiGrid,
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
debouncedSearch: debounce(function (e) {
|
||||
@@ -82,6 +89,11 @@ const EmojiPicker = {
|
||||
return list.filter(emoji => {
|
||||
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
|
||||
})
|
||||
},
|
||||
getDefaultGroup () {
|
||||
if (!this.visible) return null
|
||||
const recentEmojis = this.$store.getters.recentEmojis
|
||||
return recentEmojis.length === 0 ? 'standard' : 'recent'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -148,6 +160,13 @@ const EmojiPicker = {
|
||||
stickerPickerEnabled () {
|
||||
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (val, oldVal) {
|
||||
if (val && this.activeGroup === null) {
|
||||
this.activeGroup = this.getDefaultGroup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
|
||||
<img
|
||||
<StillImage
|
||||
v-else
|
||||
:src="group.first.imageUrl"
|
||||
>
|
||||
noStopGifs="true"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span
|
||||
<template
|
||||
v-if="reaction.url !== null"
|
||||
>
|
||||
<StillImage
|
||||
@@ -19,16 +19,15 @@
|
||||
:title="reaction.name"
|
||||
:alt="reaction.name"
|
||||
class="reaction-emoji"
|
||||
height="2.55em"
|
||||
/>
|
||||
{{ reaction.count }}
|
||||
</span>
|
||||
<span v-else>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</UserListPopover>
|
||||
<a
|
||||
@@ -53,23 +52,26 @@
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 210%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
padding: 2px 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
align-items: end;
|
||||
|
||||
.reaction-emoji {
|
||||
width: auto;
|
||||
max-width: 96cqw;
|
||||
height: 2.55em !important;
|
||||
margin-right: 0.25em;
|
||||
|
||||
&.still-image {
|
||||
height: 2.55em;
|
||||
}
|
||||
&.unicode-emoji {
|
||||
display: inline-block;
|
||||
font-size: 2.125em; // assuming default line height of 1.2rem and emojis that don't exceed line height
|
||||
line-height: 2.55rem;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
@@ -97,9 +99,9 @@
|
||||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
&, &:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--accent, $fallback--link);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
@@ -172,7 +172,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<basic-user-card :user="user" v-if="show">
|
||||
<basic-user-card
|
||||
v-if="show"
|
||||
:user="user"
|
||||
>
|
||||
<div class="follow-request-card-content-container">
|
||||
<button
|
||||
class="btn button-default"
|
||||
|
||||
@@ -88,10 +88,8 @@ const Gallery = {
|
||||
set(this.sizes, id, { width, height })
|
||||
},
|
||||
rowStyle (row) {
|
||||
if (row.audio) {
|
||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||
} else if (!row.minimal && !row.grid) {
|
||||
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
|
||||
if (!row.audio && !row.minimal && !row.grid) {
|
||||
return { 'aspect-ratio': `1/${(1 / (row.items.length + 0.6))}` }
|
||||
}
|
||||
},
|
||||
itemStyle (id, row) {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
:description="descriptions && descriptions[attachment.id]"
|
||||
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
||||
:style="itemStyle(attachment.id, row.items)"
|
||||
@setMedia="onMedia"
|
||||
@naturalSizeLoad="onNaturalSizeLoad"
|
||||
@set-media="onMedia"
|
||||
@natural-size-load="onNaturalSizeLoad"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,9 +96,15 @@
|
||||
|
||||
.gallery-row {
|
||||
position: relative;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
.Status & {
|
||||
max-height: 30em;
|
||||
}
|
||||
|
||||
&.-audio {
|
||||
aspect-ratio: 4/1; // this is terrible, but it's how it was before so I'm not changing it >:(
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5em;
|
||||
|
||||
@@ -42,6 +42,7 @@ export default {
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.list {
|
||||
min-height: 1em;
|
||||
&-item:not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Modal
|
||||
v-if="showing"
|
||||
class="media-modal-view"
|
||||
@backdropClicked="hideIfNotSwiped"
|
||||
@backdrop-clicked="hideIfNotSwiped"
|
||||
>
|
||||
<SwipeClick
|
||||
v-if="type === 'image'"
|
||||
@@ -24,14 +24,15 @@
|
||||
:min-scale="pinchZoomMinScale"
|
||||
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
|
||||
>
|
||||
<img
|
||||
<StillImage
|
||||
:class="{ loading }"
|
||||
class="modal-image"
|
||||
:src="currentMedia.url"
|
||||
:alt="currentMedia.description"
|
||||
:title="currentMedia.description"
|
||||
@load="onImageLoaded"
|
||||
>
|
||||
noStopGifs="true"
|
||||
/>
|
||||
</PinchZoom>
|
||||
</SwipeClick>
|
||||
<VideoAttachment
|
||||
|
||||
@@ -42,7 +42,7 @@ const mediaUpload = {
|
||||
.then((fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => { // eslint-disable-line handle-callback-err
|
||||
}, (error) => {
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
|
||||
@@ -93,9 +93,6 @@ const MentionLink = {
|
||||
this.highlightType
|
||||
]
|
||||
},
|
||||
useAtIcon () {
|
||||
return this.mergedConfig.useAtIcon
|
||||
},
|
||||
isRemote () {
|
||||
return this.userName !== this.userNameFull
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<input
|
||||
id="code"
|
||||
v-model="code"
|
||||
autocomplete="one-time-code"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="panel-heading"
|
||||
@click="toggleHidden"
|
||||
>
|
||||
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
|
||||
<h4>{{ $t('moderation.reports.report') + ' ' + account.screen_name }}</h4>
|
||||
<button
|
||||
v-if="isOpen"
|
||||
class="button-default"
|
||||
@@ -35,7 +35,10 @@
|
||||
<div v-if="content">
|
||||
{{ decode(content) }}
|
||||
</div>
|
||||
<i v-else class="faint">
|
||||
<i
|
||||
v-else
|
||||
class="faint"
|
||||
>
|
||||
{{ $t('moderation.reports.no_content') }}
|
||||
</i>
|
||||
<div class="report-author">
|
||||
@@ -43,12 +46,12 @@
|
||||
class="small-avatar"
|
||||
:user="actor"
|
||||
/>
|
||||
{{ this.actor.screen_name }}
|
||||
{{ actor.screen_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden && statuses.length > 0"
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.statuses.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@@ -74,8 +77,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden && notes.length > 0"
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.notes.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@@ -99,9 +102,9 @@
|
||||
</div>
|
||||
<div class="report-add-note">
|
||||
<textarea
|
||||
v-model.trim="note"
|
||||
rows="1"
|
||||
cols="1"
|
||||
v-model.trim="note"
|
||||
:placeholder="$t('moderation.reports.note_placeholder')"
|
||||
/>
|
||||
<button
|
||||
@@ -134,7 +137,7 @@
|
||||
:offset="{ y: 5 }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
@@ -147,7 +150,7 @@
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{close}">
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="small-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
{{ this.user.screen_name }}
|
||||
{{ user.screen_name }}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Timeago
|
||||
|
||||
@@ -22,6 +22,9 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'backdropClicked',
|
||||
],
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@show="setToggled(true)"
|
||||
@close="setToggled(false)"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
@@ -122,7 +122,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="btn button-default btn-block moderation-tools-button"
|
||||
:class="{ toggled }"
|
||||
@@ -137,11 +137,11 @@
|
||||
v-if="showDeleteUserDialog"
|
||||
:on-cancel="deleteUserDialog.bind(this, false)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<template #header>
|
||||
{{ $t('user_card.admin_menu.delete_user') }}
|
||||
</template>
|
||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||
<template v-slot:footer>
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="deleteUserDialog(false)"
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
@@ -50,7 +51,8 @@ const Notification = {
|
||||
Timeago,
|
||||
Status,
|
||||
RichContent,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
||||
@@ -101,4 +101,8 @@
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.attachment-wrapper {
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,12 +116,13 @@
|
||||
scope="global"
|
||||
keypath="notifications.reacted_with"
|
||||
>
|
||||
<img
|
||||
<still-image
|
||||
v-if="notification.emoji_url !== null"
|
||||
class="notification-reaction-emoji"
|
||||
:src="notification.emoji_url"
|
||||
:name="notification.emoji"
|
||||
>
|
||||
:title="notification.emoji"
|
||||
:alt="notification.emoji"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-reaction-emoji"
|
||||
@@ -151,7 +152,6 @@
|
||||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:with-direction="true"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
class="button-default dropdown-item"
|
||||
@@ -72,7 +72,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="filter-trigger-button button-unstyled">
|
||||
<FAIcon icon="filter" />
|
||||
</button>
|
||||
|
||||
@@ -105,9 +105,12 @@
|
||||
flex: 1;
|
||||
padding-left: 0.8em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.heading-right, .notification-right {
|
||||
.timeago {
|
||||
min-width: 3em;
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||
@update:model-value="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<pinch-zoom
|
||||
class="pinch-zoom-parent"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot />
|
||||
</pinch-zoom>
|
||||
|
||||
@@ -50,6 +50,13 @@ export default {
|
||||
totalVotesCount () {
|
||||
return this.poll.votes_count
|
||||
},
|
||||
totalFractionBase () {
|
||||
// Due to a backend bug, we might not have any voter count info for remote polls
|
||||
// in this case, fall back to count of votes even for multiple cjoice polls
|
||||
// to be able to at least display _something_
|
||||
const total_base = this.poll.multiple ? this.poll.voters_count : this.poll.votes_count
|
||||
return total_base > 0 ? total_base : this.poll.votes_count
|
||||
},
|
||||
containerClass () {
|
||||
return {
|
||||
loading: this.loading
|
||||
@@ -70,10 +77,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
percentageForOption (count) {
|
||||
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
|
||||
const total = this.totalFractionBase
|
||||
return total === 0 ? 0 : Math.round(count / total * 100)
|
||||
},
|
||||
resultTitle (option) {
|
||||
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
|
||||
return `${option.votes_count}/${this.totalFractionBase} ${this.$t('polls.votes')}`
|
||||
},
|
||||
fetchPoll () {
|
||||
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<button
|
||||
v-if="options.length > 2"
|
||||
class="delete-option button-unstyled -hover-highlight"
|
||||
type="button"
|
||||
@click="deleteOption(index)"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
@@ -32,6 +33,7 @@
|
||||
<button
|
||||
v-if="options.length < maxOptions"
|
||||
class="add-option faint button-unstyled -hover-highlight"
|
||||
type="button"
|
||||
@click="addOption"
|
||||
>
|
||||
<FAIcon
|
||||
|
||||
@@ -9,11 +9,13 @@ import StatusContent from '../status_content/status_content.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||
import { usePostLanguageOptions } from 'src/lib/post_language'
|
||||
import scopeUtils from 'src/lib/scope_utils.js'
|
||||
import suggestor from '../emoji_input/suggestor.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Select from '../select/select.vue'
|
||||
import iso6391 from 'iso-639-1'
|
||||
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
@@ -62,6 +64,13 @@ const deleteDraft = (draftKey) => {
|
||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
const interfaceToISOLanguage = (ilang) => {
|
||||
const sep = ilang.indexOf("_");
|
||||
return sep < 0 ?
|
||||
ilang :
|
||||
ilang.substr(0, sep);
|
||||
}
|
||||
|
||||
const PostStatusForm = {
|
||||
props: [
|
||||
'statusId',
|
||||
@@ -77,6 +86,7 @@ const PostStatusForm = {
|
||||
'quoteId',
|
||||
'repliedUser',
|
||||
'attentions',
|
||||
'copyMessageLanguage',
|
||||
'copyMessageScope',
|
||||
'subject',
|
||||
'disableSubject',
|
||||
@@ -129,6 +139,13 @@ const PostStatusForm = {
|
||||
this.$refs.textarea.focus()
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const {postLanguageOptions} = usePostLanguageOptions()
|
||||
|
||||
return {
|
||||
postLanguageOptions,
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const preset = this.$route.query.message
|
||||
let statusText = preset || ''
|
||||
@@ -138,7 +155,7 @@ const PostStatusForm = {
|
||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
|
||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
|
||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||
|
||||
let statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
@@ -149,7 +166,7 @@ const PostStatusForm = {
|
||||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: this.suggestedVisibility(),
|
||||
language: interfaceLanguage,
|
||||
language: this.suggestedLanguage(),
|
||||
contentType
|
||||
}
|
||||
|
||||
@@ -164,7 +181,7 @@ const PostStatusForm = {
|
||||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || this.suggestedVisibility(),
|
||||
language: this.statusLanguage || interfaceLanguage,
|
||||
language: this.statusLanguage || this.suggestedLanguage(),
|
||||
contentType: statusContentType
|
||||
}
|
||||
}
|
||||
@@ -199,6 +216,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 {
|
||||
dropFiles: [],
|
||||
uploadingFiles: false,
|
||||
@@ -213,7 +234,10 @@ const PostStatusForm = {
|
||||
preview: null,
|
||||
previewLoading: false,
|
||||
emojiInputShown: false,
|
||||
idempotencyKey: ''
|
||||
idempotencyKey: '',
|
||||
activeEmojiInput: undefined,
|
||||
activeTextInput: undefined,
|
||||
subjectVisible: showSubject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -302,13 +326,11 @@ const PostStatusForm = {
|
||||
...mapState({
|
||||
mobileLayout: state => state.interface.mobileLayout
|
||||
}),
|
||||
isoLanguages () {
|
||||
return iso6391.getAllCodes();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'newStatus': {
|
||||
deep: true,
|
||||
flush: 'sync',
|
||||
handler () {
|
||||
this.statusChanged()
|
||||
}
|
||||
@@ -321,17 +343,22 @@ const PostStatusForm = {
|
||||
this.saveDraft()
|
||||
},
|
||||
clearStatus () {
|
||||
const newStatus = this.newStatus
|
||||
const config = this.$store.getters.mergedConfig
|
||||
this.newStatus = {
|
||||
status: '',
|
||||
spoilerText: '',
|
||||
files: [],
|
||||
visibility: newStatus.visibility,
|
||||
contentType: newStatus.contentType,
|
||||
language: newStatus.language,
|
||||
nsfw: !!config.sensitiveByDefault,
|
||||
visibility: this.suggestedVisibility(),
|
||||
contentType: config.postContentType,
|
||||
language: this.suggestedLanguage(),
|
||||
poll: {},
|
||||
mediaDescriptions: {}
|
||||
}
|
||||
const scopeselector = this.$refs.scopeselector
|
||||
if (scopeselector) {
|
||||
scopeselector.currentScope = this.newStatus.visibility
|
||||
}
|
||||
this.pollFormVisible = false
|
||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||
this.clearPollForm()
|
||||
@@ -491,7 +518,7 @@ const PostStatusForm = {
|
||||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '' || !!this.$store.getters.mergedConfig.sensitiveByDefault) {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
this.$emit('resize', { delayed: true })
|
||||
@@ -674,8 +701,33 @@ const PostStatusForm = {
|
||||
this.$refs['emoji-input'].resize()
|
||||
},
|
||||
showEmojiPicker () {
|
||||
this.$refs['textarea'].focus()
|
||||
this.$refs['emoji-input'].triggerShowPicker()
|
||||
if (!this.activeEmojiInput || !this.activeTextInput)
|
||||
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 () {
|
||||
this.error = null
|
||||
@@ -715,16 +767,19 @@ const PostStatusForm = {
|
||||
openProfileTab () {
|
||||
this.$store.dispatch('openSettingsModalTab', 'profile')
|
||||
},
|
||||
suggestedLanguage () {
|
||||
// Make sure the inherited language is actually valid
|
||||
if (this.postLanguageOptions.find(o => o.value === this.copyMessageLanguage)) {
|
||||
return this.copyMessageLanguage
|
||||
}
|
||||
const { postLanguage: defaultPostLanguage, interfaceLanguage } = this.$store.getters.mergedConfig
|
||||
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
|
||||
return postLanguage
|
||||
},
|
||||
suggestedVisibility () {
|
||||
if (this.copyMessageScope) {
|
||||
if (this.copyMessageScope === 'direct') {
|
||||
return this.copyMessageScope
|
||||
}
|
||||
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
|
||||
return this.copyMessageScope
|
||||
}
|
||||
}
|
||||
return this.$store.state.users.currentUser.default_scope
|
||||
const maxScope = this.copyMessageScope
|
||||
const defaultScope = this.$store.state.users.currentUser.default_scope
|
||||
return scopeUtils.negotiate(defaultScope, maxScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link"
|
||||
type="button"
|
||||
@click="openProfileTab"
|
||||
>
|
||||
{{ $t('post_status.account_not_locked_warning_link') }}
|
||||
@@ -118,13 +119,16 @@
|
||||
/>
|
||||
</div>
|
||||
<EmojiInput
|
||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||
v-if="subjectVisible"
|
||||
ref="subject-emoji-input"
|
||||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:suggest="emojiSuggestor"
|
||||
class="form-control"
|
||||
>
|
||||
<input
|
||||
ref="subject-input"
|
||||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
@@ -132,6 +136,8 @@
|
||||
size="1"
|
||||
class="form-post-subject"
|
||||
@input="onSubjectInput"
|
||||
@focus="focusSubjectInput()"
|
||||
@keydown.exact.enter.prevent
|
||||
>
|
||||
</EmojiInput>
|
||||
<i18n-t
|
||||
@@ -166,13 +172,14 @@
|
||||
cols="1"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
class="form-post-body"
|
||||
:class="{ 'scrollable-form': !!maxHeight }"
|
||||
:class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }"
|
||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
@focus="focusStatusInput()"
|
||||
/>
|
||||
<p
|
||||
v-if="hasStatusLengthLimit"
|
||||
@@ -185,8 +192,10 @@
|
||||
<div
|
||||
v-if="!disableScopeSelector"
|
||||
class="visibility-tray"
|
||||
:class="{ 'visibility-tray-edit': isEdit }"
|
||||
>
|
||||
<scope-selector
|
||||
ref="scopeselector"
|
||||
v-if="!disableVisibilitySelector"
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="copyMessageScope"
|
||||
@@ -195,7 +204,9 @@
|
||||
/>
|
||||
|
||||
<div
|
||||
class="language-selector"
|
||||
class="format-selector-container">
|
||||
<div
|
||||
class="format-selector"
|
||||
>
|
||||
<Select
|
||||
id="post-language"
|
||||
@@ -203,17 +214,17 @@
|
||||
class="form-control"
|
||||
>
|
||||
<option
|
||||
v-for="language in isoLanguages"
|
||||
:key="language"
|
||||
:value="language"
|
||||
v-for="language in postLanguageOptions"
|
||||
:key="language.key"
|
||||
:value="language.value"
|
||||
>
|
||||
{{ language }}
|
||||
{{ language.label }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="postFormats.length > 1"
|
||||
class="text-format"
|
||||
class="text-format format-selector"
|
||||
>
|
||||
<Select
|
||||
id="post-content-type"
|
||||
@@ -231,7 +242,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
|
||||
class="text-format"
|
||||
class="text-format format-selector"
|
||||
>
|
||||
<span class="only-format">
|
||||
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
|
||||
@@ -239,6 +250,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<poll-form
|
||||
v-if="pollsAvailable"
|
||||
ref="pollForm"
|
||||
@@ -263,6 +275,7 @@
|
||||
<button
|
||||
class="emoji-icon button-unstyled"
|
||||
:title="$t('emoji.add_emoji')"
|
||||
type="button"
|
||||
@click="showEmojiPicker"
|
||||
>
|
||||
<FAIcon icon="smile-beam" />
|
||||
@@ -272,10 +285,21 @@
|
||||
class="poll-icon button-unstyled"
|
||||
:class="{ selected: pollFormVisible }"
|
||||
:title="$t('polls.add_poll')"
|
||||
type="button"
|
||||
@click="togglePollForm"
|
||||
>
|
||||
<FAIcon icon="poll-h" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!disableSubject"
|
||||
class="spoiler-icon button-unstyled"
|
||||
:class="{ selected: subjectVisible }"
|
||||
:title="$t('post_status.toggle_content_warning')"
|
||||
type="button"
|
||||
@click="toggleSubjectVisible"
|
||||
>
|
||||
<FAIcon icon="eye-slash" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="posting"
|
||||
@@ -446,6 +470,10 @@
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visibility-tray-edit {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.visibility-notice.edit-warning {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
@@ -456,7 +484,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;
|
||||
line-height: 1.1;
|
||||
flex: 1;
|
||||
@@ -499,6 +533,11 @@
|
||||
|
||||
.poll-icon {
|
||||
order: 3;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spoiler-icon {
|
||||
order: 4;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
@@ -551,6 +590,11 @@
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.form-post-subject {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.form-post-body {
|
||||
// TODO: make a resizable textarea component?
|
||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||
@@ -563,6 +607,11 @@
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
|
||||
&.-has-subject {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="isLoggedIn && !resettingForm"
|
||||
:is-open="modalActivated"
|
||||
class="post-form-modal-view"
|
||||
@backdropClicked="closeModal"
|
||||
@backdrop-clicked="closeModal"
|
||||
>
|
||||
<div class="post-form-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
remove-padding
|
||||
@show="focusInput"
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{close}">
|
||||
<EmojiPicker
|
||||
:enable-sticker-picker="false"
|
||||
@emoji="addReaction($event, close)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button-unstyled popover-trigger"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
props: [ 'user' ],
|
||||
computed: {
|
||||
subscribeUrl () {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
const serverUrl = new URL(this.user.statusnet_profile_url)
|
||||
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||
import scopeUtils from 'src/lib/scope_utils.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faRetweet } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
@@ -7,12 +9,14 @@ library.add(faRetweet)
|
||||
const RetweetButton = {
|
||||
props: ['status', 'loggedIn', 'visibility'],
|
||||
components: {
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
ScopeSelector
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
animated: false,
|
||||
showingConfirmDialog: false
|
||||
showingConfirmDialog: false,
|
||||
retweetVisibility: this.$store.state.users.currentUser.default_scope
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -25,7 +29,7 @@ const RetweetButton = {
|
||||
},
|
||||
doRetweet () {
|
||||
if (!this.status.repeated) {
|
||||
this.$store.dispatch('retweet', { id: this.status.id })
|
||||
this.$store.dispatch('retweet', { id: this.status.id, visibility: this.retweetVisibility })
|
||||
} else {
|
||||
this.$store.dispatch('unretweet', { id: this.status.id })
|
||||
}
|
||||
@@ -40,6 +44,9 @@ const RetweetButton = {
|
||||
},
|
||||
hideConfirmDialog () {
|
||||
this.showingConfirmDialog = false
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.retweetVisibility = visibility
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -54,6 +61,15 @@ const RetweetButton = {
|
||||
},
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
},
|
||||
userDefaultScope () {
|
||||
return this.$store.state.users.currentUser.default_scope
|
||||
},
|
||||
statusScope () {
|
||||
return this.status.visibility
|
||||
},
|
||||
initialScope () {
|
||||
return scopeUtils.negotiate(this.userDefaultScope, this.status.visibility)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
@cancelled="hideConfirmDialog"
|
||||
>
|
||||
{{ $t('status.repeat_confirm') }}
|
||||
<scope-selector
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="statusScope"
|
||||
:initial-scope="initialScope"
|
||||
:on-scope-change="changeVis"
|
||||
/>
|
||||
</confirm-modal>
|
||||
</teleport>
|
||||
</div>
|
||||
|
||||
@@ -121,6 +121,19 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const mfmStyleFromDataAttributes = (attributes) => {
|
||||
// CSS selectors can check if a data-* attribute is true, but can't use other values, so we want to add them to the style attribute
|
||||
// Here we turn e.g. `{'data-mfm-some': '1deg', 'data-mfm-thing': '5s'}` to "--mfm-some: 1deg;--mfm-thing: 5s;"
|
||||
// Note that we only add the value to `style` when they contain only letters, numbers, dot, or minus signs
|
||||
// At the moment of writing, this should be enough for legitimate purposes and reduces the chance of injection by using special characters
|
||||
// There is a special case for the `color` value, who is provided without `#`, but requires this in the `style` attribute
|
||||
return Object.keys(attributes).filter(
|
||||
(key) => key.startsWith('data-mfm-') && attributes[key] !== true && /^[a-zA-Z0-9.\-]*$/.test(attributes[key])
|
||||
).map(
|
||||
(key) => '--mfm-' + key.substr(9) + (key === 'data-mfm-color' ? ': #' : ': ') + attributes[key] + ';'
|
||||
).reduce((a,v) => a+v, '')
|
||||
}
|
||||
|
||||
// Processor to use with html_tree_converter
|
||||
const processItem = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
@@ -191,6 +204,15 @@ export default {
|
||||
if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) {
|
||||
return ['', children.map(processItem), '']
|
||||
}
|
||||
|
||||
let mfm_style = mfmStyleFromDataAttributes(attrs)
|
||||
if (mfm_style !== '') {
|
||||
return [
|
||||
opener.slice(0,-1) + ' style="' + mfm_style + '">',
|
||||
children.map(processItem),
|
||||
closer
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (children !== undefined) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import scopeUtils from 'src/lib/scope_utils.js'
|
||||
|
||||
library.add(
|
||||
faEnvelope,
|
||||
faGlobe,
|
||||
@@ -13,18 +15,11 @@ library.add(
|
||||
faLockOpen
|
||||
)
|
||||
|
||||
const SCOPE_LEVELS = {
|
||||
'direct': 0,
|
||||
'private': 1,
|
||||
'local': 2,
|
||||
'unlisted': 2,
|
||||
'public': 3
|
||||
}
|
||||
|
||||
const ScopeSelector = {
|
||||
props: [
|
||||
'showAll',
|
||||
'userDefault',
|
||||
// scope of parent object
|
||||
'originalScope',
|
||||
'initialScope',
|
||||
'onScopeChange'
|
||||
@@ -39,16 +34,16 @@ const ScopeSelector = {
|
||||
return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
|
||||
},
|
||||
showPublic () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('public')
|
||||
return this.shouldShow('public')
|
||||
},
|
||||
showLocal () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('local')
|
||||
return this.shouldShow('local')
|
||||
},
|
||||
showUnlisted () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('unlisted')
|
||||
return this.shouldShow('unlisted')
|
||||
},
|
||||
showPrivate () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('private')
|
||||
return this.shouldShow('private')
|
||||
},
|
||||
showDirect () {
|
||||
return this.shouldShow('direct')
|
||||
@@ -65,15 +60,10 @@ const ScopeSelector = {
|
||||
},
|
||||
methods: {
|
||||
shouldShow (scope) {
|
||||
if (!this.originalScope) {
|
||||
if (!this.originalScope)
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.originalScope === 'local') {
|
||||
return scope === 'direct' || scope === 'local'
|
||||
}
|
||||
|
||||
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
|
||||
else
|
||||
return scopeUtils.compare(scope, this.originalScope) <= 0
|
||||
},
|
||||
changeVis (scope) {
|
||||
this.currentScope = scope
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
:items="items"
|
||||
:get-key="getKey"
|
||||
>
|
||||
<template v-slot:item="{item}">
|
||||
<template #item="{item}">
|
||||
<div
|
||||
class="selectable-list-item-inner"
|
||||
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
|
||||
@@ -41,7 +41,7 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:empty>
|
||||
<template #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</List>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Checkbox
|
||||
:model-value="state"
|
||||
:disabled="disabled"
|
||||
@update:modelValue="update"
|
||||
@update:model-value="update"
|
||||
>
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<Select
|
||||
:model-value="state"
|
||||
:disabled="disabled"
|
||||
@update:modelValue="update"
|
||||
@update:model-value="update"
|
||||
>
|
||||
<option
|
||||
v-for="option in options"
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<Popover
|
||||
trigger="hover"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon
|
||||
icon="wrench"
|
||||
:aria-label="$t('settings.setting_changed')"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="modified-tooltip">
|
||||
{{ $t('settings.setting_changed') }}
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<Popover
|
||||
trigger="hover"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon
|
||||
icon="server"
|
||||
:aria-label="$t('settings.setting_server_side')"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="serverside-tooltip">
|
||||
{{ $t('settings.setting_server_side') }}
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ const SettingsModal = {
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
},
|
||||
logout () {
|
||||
this.$router.replace('/main/public')
|
||||
this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
this.$store.dispatch('logout')
|
||||
},
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<Checkbox
|
||||
:model-value="!!expertLevel"
|
||||
class="expertMode"
|
||||
@update:modelValue="expertLevel = Number($event)"
|
||||
@update:model-value="expertLevel = Number($event)"
|
||||
>
|
||||
{{ $t("settings.expert_mode") }}
|
||||
</Checkbox>
|
||||
|
||||
@@ -72,7 +72,7 @@ const DataImportExportTab = {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
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 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 ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
@@ -17,6 +18,11 @@ library.add(
|
||||
)
|
||||
|
||||
const GeneralTab = {
|
||||
setup() {
|
||||
const {postLanguageOptions} = usePostLanguageOptions()
|
||||
|
||||
return {postLanguageOptions}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
|
||||
@@ -118,6 +124,12 @@ const GeneralTab = {
|
||||
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()
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<template
|
||||
v-if="profilesExpanded"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="profile in settingsProfiles"
|
||||
:key="profile.id"
|
||||
@@ -73,15 +72,24 @@
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<button class="btn button-default" @click="refreshProfiles()">
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="refreshProfiles()"
|
||||
>
|
||||
{{ $t('settings.settings_profiles_refresh') }}
|
||||
<FAIcon icon="sync" @click="refreshProfiles()" />
|
||||
<FAIcon
|
||||
icon="sync"
|
||||
@click="refreshProfiles()"
|
||||
/>
|
||||
</button>
|
||||
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
|
||||
<label for="settings-profile-new-name">
|
||||
{{ $t('settings.settings_profile_creation_new_name_label') }}
|
||||
</label>
|
||||
<input v-model="newProfileName" id="settings-profile-new-name">
|
||||
<input
|
||||
id="settings-profile-new-name"
|
||||
v-model="newProfileName"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="createSettingsProfile"
|
||||
@@ -146,6 +154,21 @@
|
||||
{{ $t('settings.show_wider_shortcuts') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="displayPageBackgrounds">
|
||||
{{ $t('settings.show_page_backgrounds') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="centerAlignBio">
|
||||
{{ $t('settings.center_align_bio') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="compactUserInfo">
|
||||
{{ $t('settings.compact_user_info') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
@@ -256,6 +279,11 @@
|
||||
{{ $t('settings.right_sidebar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="widenTimeline">
|
||||
{{ $t('settings.widen_timeline') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
@@ -483,14 +511,6 @@
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useAtIcon"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.use_at_icon') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkShowAvatar">
|
||||
{{ $t('settings.mention_link_show_avatar') }}
|
||||
@@ -588,6 +608,15 @@
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="postLanguage"
|
||||
path="postLanguage"
|
||||
:options="postLanguageOptions"
|
||||
>
|
||||
{{ $t('settings.post_language') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysShowNewPostButton"
|
||||
|
||||
@@ -85,7 +85,7 @@ const MutesAndBlocks = {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_block')"
|
||||
>
|
||||
<template v-slot="row">
|
||||
<template #default="row">
|
||||
<BlockCard
|
||||
:user-id="row.item"
|
||||
/>
|
||||
@@ -21,7 +21,7 @@
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template v-slot:header="{selected}">
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
@@ -29,7 +29,7 @@
|
||||
:click="() => blockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
@@ -39,16 +39,16 @@
|
||||
:click="() => unblockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item="{item}">
|
||||
<template #item="{item}">
|
||||
<BlockCard :user-id="item" />
|
||||
</template>
|
||||
<template v-slot:empty>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
@@ -63,7 +63,7 @@
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<template v-slot="row">
|
||||
<template #default="row">
|
||||
<MuteCard
|
||||
:user-id="row.item"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template v-slot:header="{selected}">
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
@@ -82,7 +82,7 @@
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
@@ -92,16 +92,16 @@
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item="{item}">
|
||||
<template #item="{item}">
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template v-slot:empty>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
@@ -114,7 +114,7 @@
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
>
|
||||
<template v-slot="row">
|
||||
<template #default="row">
|
||||
<DomainMuteCard
|
||||
:domain="row.item"
|
||||
/>
|
||||
@@ -125,7 +125,7 @@
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template v-slot:header="{selected}">
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
@@ -133,16 +133,16 @@
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item="{item}">
|
||||
<template #item="{item}">
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template v-slot:empty>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
|
||||
@@ -33,6 +33,7 @@ const ProfileTab = {
|
||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
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 })),
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
@@ -129,14 +130,15 @@ const ProfileTab = {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
bot: this.bot,
|
||||
show_role: this.showRole,
|
||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
||||
permit_followback: this.permit_followback,
|
||||
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
}
|
||||
|
||||
if (this.emailLanguage) {
|
||||
@@ -185,7 +187,7 @@ const ProfileTab = {
|
||||
})
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
|
||||
@@ -110,11 +110,9 @@
|
||||
max="730"
|
||||
class="expire-posts-days"
|
||||
:placeholder="$t('settings.expire_posts_input_placeholder')"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
>
|
||||
</p>
|
||||
<p />
|
||||
<p>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.email_language')"
|
||||
@@ -259,6 +257,19 @@
|
||||
<BooleanSetting path="serverSide_locked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</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>
|
||||
<BooleanSetting path="serverSide_discoverable">
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { extractCommit } from 'src/services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
|
||||
function joinURL(base, subpath) {
|
||||
return URL.parse(subpath, base)?.href || "invalid base URL"
|
||||
}
|
||||
|
||||
const VersionTab = {
|
||||
data () {
|
||||
const instance = this.$store.state.instance
|
||||
return {
|
||||
backendCommitUrl: instance.backendCommitUrl,
|
||||
backendVersion: instance.backendVersion,
|
||||
frontendCommitUrl: instance.frontendCommitUrl,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
return joinURL(this.frontendCommitUrl, this.frontendVersion)
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
return joinURL(this.backendCommitUrl, extractCommit(this.backendVersion))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,16 @@
|
||||
color: $fallback--cGreen;
|
||||
color: var(--cGreen, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.repeat-tooltip {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.repeater-avatar {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
:user="statusoid.user"
|
||||
/>
|
||||
<div class="right-side faint">
|
||||
<span
|
||||
<div
|
||||
class="status-username repeater-name"
|
||||
:title="retweeter"
|
||||
>
|
||||
@@ -100,8 +100,12 @@
|
||||
v-else
|
||||
:to="retweeterProfileLink"
|
||||
>{{ retweeter }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
{{ ' ' }}
|
||||
|
||||
<div
|
||||
class="repeat-tooltip"
|
||||
>
|
||||
<FAIcon
|
||||
icon="retweet"
|
||||
class="repeat-icon"
|
||||
@@ -110,6 +114,7 @@
|
||||
{{ $t('timeline.repeated') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!deleted"
|
||||
@@ -190,7 +195,7 @@
|
||||
>
|
||||
<Timeago
|
||||
:time="status.created_at"
|
||||
:with-direction="true"
|
||||
:with-direction="!compact"
|
||||
:auto-update="60"
|
||||
/>
|
||||
</router-link>
|
||||
@@ -368,7 +373,7 @@
|
||||
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
@parse-ready="setHeadTailLinks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -476,8 +481,8 @@
|
||||
/>
|
||||
<extra-buttons
|
||||
:status="status"
|
||||
@onError="showError"
|
||||
@onSuccess="clearError"
|
||||
@on-error="showError"
|
||||
@on-success="clearError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -514,6 +519,7 @@
|
||||
:reply-to="status.id"
|
||||
:attentions="status.attentions"
|
||||
:replied-user="status.user"
|
||||
:copy-message-language="status.language"
|
||||
:copy-message-scope="status.visibility"
|
||||
:subject="replySubject"
|
||||
@posted="toggleReplying"
|
||||
@@ -528,6 +534,7 @@
|
||||
:quote-id="status.id"
|
||||
:attentions="[status.user]"
|
||||
:replied-user="status.user"
|
||||
:copy-message-language="status.language"
|
||||
:copy-message-scope="status.visibility"
|
||||
:subject="replySubject"
|
||||
@posted="toggleQuoting"
|
||||
|
||||
@@ -41,7 +41,8 @@ const StatusContent = {
|
||||
postLength: this.status.text.length,
|
||||
parseReadyDone: false,
|
||||
renderMisskeyMarkdown,
|
||||
translateFrom: null
|
||||
translateFrom: null,
|
||||
translating: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -135,7 +136,10 @@ const StatusContent = {
|
||||
},
|
||||
translateStatus () {
|
||||
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
|
||||
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom })
|
||||
this.translating = true
|
||||
this.$store.dispatch(
|
||||
'translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom }
|
||||
).finally(() => { this.translating = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.StatusBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.translation {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
@@ -23,24 +24,6 @@
|
||||
transition: 0.05s;
|
||||
}
|
||||
|
||||
._mfm_x2_ {
|
||||
.emoji {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x3_ {
|
||||
.emoji {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x4_ {
|
||||
.emoji {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="StatusBody"
|
||||
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
|
||||
:class="{ '-compact': compact }"
|
||||
>
|
||||
<div class="body">
|
||||
<div
|
||||
@@ -54,7 +54,7 @@
|
||||
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
||||
:greentext="mergedConfig.greentext"
|
||||
:attentions="status.attentions"
|
||||
@parseReady="onParseReady"
|
||||
@parse-ready="onParseReady"
|
||||
/>
|
||||
<div
|
||||
v-if="status.translation"
|
||||
@@ -70,7 +70,7 @@
|
||||
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
|
||||
:greentext="mergedConfig.greentext"
|
||||
:attentions="status.attentions"
|
||||
@parseReady="onParseReady"
|
||||
@parse-ready="onParseReady"
|
||||
/>
|
||||
<div>
|
||||
<label class="label">{{ $t('status.override_translation_source_language') }}</label>
|
||||
@@ -89,7 +89,11 @@
|
||||
</option>
|
||||
</Select>
|
||||
{{ ' ' }}
|
||||
<button @click="translateStatus" class="btn button-default">
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="translating"
|
||||
@click="translateStatus"
|
||||
>
|
||||
{{ $t('status.translate') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* "FEP-c16b: Formatting MFM functions" attributes that Akkoma supports
|
||||
*/
|
||||
|
||||
.StatusContent:not(.mfm-disabled) {
|
||||
/* The following are the non-animated MFM */
|
||||
.mfm-center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mfm-flip {
|
||||
display: inline-block;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.mfm-flip[data-mfm-v] {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.mfm-flip[data-mfm-v][data-mfm-h] {
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-serif] {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-monospace] {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-cursive] {
|
||||
font-family: cursive;
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-fantasy] {
|
||||
font-family: fantasy;
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-emoji] {
|
||||
font-family: emoji;
|
||||
}
|
||||
|
||||
.mfm-font[data-mfm-math] {
|
||||
font-family: math;
|
||||
}
|
||||
|
||||
.mfm-blur {
|
||||
filter: blur(6px);
|
||||
transition: filter 0.3s;
|
||||
|
||||
&:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.mfm-rotate {
|
||||
display: inline-block;
|
||||
transform: rotate(calc(var(--mfm-deg, 90) * 1deg));
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.mfm-x2 {
|
||||
--mfm-zoom-size: 200%;
|
||||
}
|
||||
|
||||
.mfm-x3 {
|
||||
--mfm-zoom-size: 400%;
|
||||
}
|
||||
|
||||
.mfm-x4 {
|
||||
--mfm-zoom-size: 600%;
|
||||
}
|
||||
|
||||
.mfm-x2,
|
||||
.mfm-x3,
|
||||
.mfm-x4,
|
||||
.mfm-tada {
|
||||
.emoji {
|
||||
--emoji-size: 2em;
|
||||
}
|
||||
font-size: var(--mfm-zoom-size);
|
||||
|
||||
.mfm-x2,
|
||||
.mfm-x3,
|
||||
.mfm-x4,
|
||||
.mfm-tada {
|
||||
/* only half effective */
|
||||
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
|
||||
|
||||
.mfm-x2,
|
||||
.mfm-x3,
|
||||
.mfm-x4,
|
||||
.mfm-tada {
|
||||
/* disabled */
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mfm-position {
|
||||
display: inline-block;
|
||||
transform: translate(calc(var(--mfm-x, 0) * 1em), calc(var(--mfm-y, 0) * 1em));
|
||||
}
|
||||
|
||||
.mfm-scale {
|
||||
display: inline-block;
|
||||
transform: scale(var(--mfm-x, 1), var(--mfm-y, 1));
|
||||
}
|
||||
|
||||
.mfm-fg {
|
||||
color: var(--mfm-color, #f00);
|
||||
}
|
||||
|
||||
.mfm-bg {
|
||||
background-color: var(--mfm-color, #0f0);
|
||||
}
|
||||
|
||||
/* The following are the animated MFM */
|
||||
|
||||
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
|
||||
* So either StatusContent does not have this class,
|
||||
* or it has the class and we are hovering over StatusContent
|
||||
*/
|
||||
&:not(.mfm-hover:not(:hover)) {
|
||||
.mfm-jelly {
|
||||
display: inline-block;
|
||||
animation: mfm-rubberBand var(--mfm-speed, 1s) linear infinite both;
|
||||
}
|
||||
|
||||
.mfm-twitch {
|
||||
display: inline-block;
|
||||
animation: mfm-twitch var(--mfm-speed, 0.5s) ease infinite;
|
||||
}
|
||||
|
||||
.mfm-shake {
|
||||
display: inline-block;
|
||||
animation: mfm-shake var(--mfm-speed, 0.5s) ease infinite;
|
||||
}
|
||||
|
||||
.mfm-spin {
|
||||
display: inline-block;
|
||||
animation: mfm-spin var(--mfm-speed, 1.5s) linear infinite;
|
||||
}
|
||||
|
||||
.mfm-spin[data-mfm-y] {
|
||||
animation-name: mfm-spinY;
|
||||
}
|
||||
|
||||
.mfm-spin[data-mfm-x] {
|
||||
animation-name: mfm-spinX;
|
||||
}
|
||||
|
||||
.mfm-spin[data-mfm-alternate] {
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.mfm-spin[data-mfm-left] {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.mfm-jump {
|
||||
display: inline-block;
|
||||
animation: mfm-jump var(--mfm-speed, 0.75s) linear infinite;
|
||||
}
|
||||
|
||||
.mfm-bounce {
|
||||
display: inline-block;
|
||||
animation: mfm-bounce var(--mfm-speed, 0.75s) linear infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.mfm-rainbow {
|
||||
animation: mfm-rainbow var(--mfm-speed, 1s) linear infinite;
|
||||
}
|
||||
|
||||
.mfm-tada {
|
||||
display: inline-block;
|
||||
animation: mfm-tada var(--mfm-speed, 1s) linear infinite both;
|
||||
|
||||
--mfm-zoom-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
/* animation keyframes */
|
||||
|
||||
@keyframes mfm-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-spinX {
|
||||
0% { transform: perspective(128px) rotateX(0deg); }
|
||||
100% { transform: perspective(128px) rotateX(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-spinY {
|
||||
0% { transform: perspective(128px) rotateY(0deg); }
|
||||
100% { transform: perspective(128px) rotateY(360deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-jump {
|
||||
0% { transform: translateY(0); }
|
||||
25% { transform: translateY(-16px); }
|
||||
50% { transform: translateY(0); }
|
||||
75% { transform: translateY(-8px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes mfm-bounce {
|
||||
0% { transform: translateY(0) scale(1, 1); }
|
||||
25% { transform: translateY(-16px) scale(1, 1); }
|
||||
50% { transform: translateY(0) scale(1, 1); }
|
||||
75% { transform: translateY(0) scale(1.5, 0.75); }
|
||||
100% { transform: translateY(0) scale(1, 1); }
|
||||
}
|
||||
|
||||
@keyframes mfm-twitch {
|
||||
0% { transform: translate(7px, -2px); }
|
||||
5% { transform: translate(-3px, 1px); }
|
||||
10% { transform: translate(-7px, -1px); }
|
||||
15% { transform: translate(0, -1px); }
|
||||
20% { transform: translate(-8px, 6px); }
|
||||
25% { transform: translate(-4px, -3px); }
|
||||
30% { transform: translate(-4px, -6px); }
|
||||
35% { transform: translate(-8px, -8px); }
|
||||
40% { transform: translate(4px, 6px); }
|
||||
45% { transform: translate(-3px, 1px); }
|
||||
50% { transform: translate(2px, -10px); }
|
||||
55% { transform: translate(-7px, 0); }
|
||||
60% { transform: translate(-2px, 4px); }
|
||||
65% { transform: translate(3px, -8px); }
|
||||
70% { transform: translate(6px, 7px); }
|
||||
75% { transform: translate(-7px, -2px); }
|
||||
80% { transform: translate(-7px, -8px); }
|
||||
85% { transform: translate(9px, 3px); }
|
||||
90% { transform: translate(-3px, -2px); }
|
||||
95% { transform: translate(-10px, 2px); }
|
||||
100% { transform: translate(-2px, -6px); }
|
||||
}
|
||||
|
||||
@keyframes mfm-shake {
|
||||
0% { transform: translate(-3px, -1px) rotate(-8deg); }
|
||||
5% { transform: translate(0, -1px) rotate(-10deg); }
|
||||
10% { transform: translate(1px, -3px) rotate(0deg); }
|
||||
15% { transform: translate(1px, 1px) rotate(11deg); }
|
||||
20% { transform: translate(-2px, 1px) rotate(1deg); }
|
||||
25% { transform: translate(-1px, -2px) rotate(-2deg); }
|
||||
30% { transform: translate(-1px, 2px) rotate(-3deg); }
|
||||
35% { transform: translate(2px, 1px) rotate(6deg); }
|
||||
40% { transform: translate(-2px, -3px) rotate(-9deg); }
|
||||
45% { transform: translate(0, -1px) rotate(-12deg); }
|
||||
50% { transform: translate(1px, 2px) rotate(10deg); }
|
||||
55% { transform: translate(0, -3px) rotate(8deg); }
|
||||
60% { transform: translate(1px, -1px) rotate(8deg); }
|
||||
65% { transform: translate(0, -1px) rotate(-7deg); }
|
||||
70% { transform: translate(-1px, -3px) rotate(6deg); }
|
||||
75% { transform: translate(0, -2px) rotate(4deg); }
|
||||
80% { transform: translate(-2px, -1px) rotate(3deg); }
|
||||
85% { transform: translate(1px, -3px) rotate(-10deg); }
|
||||
90% { transform: translate(1px, 0) rotate(3deg); }
|
||||
95% { transform: translate(-2px, 0) rotate(-3deg); }
|
||||
100% { transform: translate(2px, 1px) rotate(2deg); }
|
||||
}
|
||||
|
||||
@keyframes mfm-rubberBand {
|
||||
0% { transform: scale3d(1, 1, 1); }
|
||||
30% { transform: scale3d(1.25, 0.75, 1); }
|
||||
40% { transform: scale3d(0.75, 1.25, 1); }
|
||||
50% { transform: scale3d(1.15, 0.85, 1); }
|
||||
65% { transform: scale3d(0.95, 1.05, 1); }
|
||||
75% { transform: scale3d(1.05, 0.95, 1); }
|
||||
100% { transform: scale3d(1, 1, 1); }
|
||||
}
|
||||
|
||||
@keyframes mfm-rainbow {
|
||||
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
||||
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
||||
}
|
||||
|
||||
@keyframes mfm-tada {
|
||||
0%,
|
||||
100% { transform: scale3d(1, 1, 1); }
|
||||
|
||||
10%,
|
||||
20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
|
||||
|
||||
40%,
|
||||
60%,
|
||||
80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy MFM
|
||||
* This is for backwards compatibility with posts formatted on Akkoma before support for FEP-c16b
|
||||
* Note that it uses the keyframes as defined above for the FEP-c16b compatible MFM representation
|
||||
*/
|
||||
|
||||
.mfm {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* The following are the legacy non-animated MFM */
|
||||
._mfm_flip_[data-h][data-v] {
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
._mfm_flip_[data-v] {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
._mfm_flip_:not([data-v]) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
._mfm_x2_ {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
._mfm_x3_ {
|
||||
font-size: 400%;
|
||||
}
|
||||
|
||||
._mfm_x4_ {
|
||||
font-size: 600%;
|
||||
}
|
||||
|
||||
._mfm_x2_ {
|
||||
.emoji {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x3_ {
|
||||
.emoji {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x4_ {
|
||||
.emoji {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_blur_ {
|
||||
filter: blur(6px);
|
||||
transition: filter 0.3s;
|
||||
}
|
||||
|
||||
._mfm_blur_:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
._mfm_rotate_ {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* The following are the legacy animated MFM */
|
||||
|
||||
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
|
||||
* So either StatusContent does not have this class,
|
||||
* or it has the class and we are hovering over StatusContent
|
||||
*/
|
||||
&:not(.mfm-hover:not(:hover)) {
|
||||
._mfm_tada_ {
|
||||
font-size: 150%;
|
||||
animation: mfm-tada 1s linear infinite both;
|
||||
}
|
||||
|
||||
._mfm_jelly_ {
|
||||
animation: mfm-rubberBand 1s linear infinite both;
|
||||
}
|
||||
|
||||
._mfm_twitch_ {
|
||||
animation: mfm-twitch 0.5s ease infinite;
|
||||
}
|
||||
|
||||
._mfm_shake_ {
|
||||
animation: mfm-shake 0.5s ease infinite;
|
||||
}
|
||||
|
||||
._mfm_spin_ {
|
||||
animation: mfm-spin 0.5s linear infinite;
|
||||
}
|
||||
|
||||
._mfm_spin_[data-x] {
|
||||
animation-name: mfm-spinX;
|
||||
}
|
||||
|
||||
._mfm_spin_[data-y] {
|
||||
animation-name: mfm-spinY;
|
||||
}
|
||||
|
||||
._mfm_spin_[left] {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
._mfm_spin_[alternate] {
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
._mfm_jump_ {
|
||||
animation: mfm-jump 0.75s linear infinite;
|
||||
}
|
||||
|
||||
._mfm_bounce_ {
|
||||
animation: mfm-bounce 0.75s linear infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
._mfm_rainbow_ {
|
||||
animation: mfm-rainbow 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
:toggle-showing-tall="toggleShowingTall"
|
||||
:toggle-expanding-subject="toggleExpandingSubject"
|
||||
:toggle-showing-long-subject="toggleShowingLongSubject"
|
||||
@parseReady="$emit('parseReady', $event)"
|
||||
@parse-ready="$emit('parseReady', $event)"
|
||||
>
|
||||
<div v-if="status.poll && status.poll.options && !compact">
|
||||
<Poll
|
||||
@@ -64,6 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script src="./status_content.js"></script>
|
||||
<style lang="scss" src="./mfm.scss" />
|
||||
<style lang="scss">
|
||||
.StatusContent {
|
||||
flex: 1;
|
||||
@@ -75,23 +76,6 @@
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mfm-hover:not(:hover) {
|
||||
.mfm {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
&.mfm-disabled {
|
||||
span {
|
||||
font-size: 100% !important;
|
||||
}
|
||||
.mfm {
|
||||
animation: none !important;
|
||||
}
|
||||
.emoji {
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quote-inline,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Modal
|
||||
v-if="modalActivated"
|
||||
class="status-history-modal-view"
|
||||
@backdropClicked="closeModal"
|
||||
@backdrop-clicked="closeModal"
|
||||
>
|
||||
<div class="status-history-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
@@ -17,7 +17,7 @@
|
||||
v-for="status in history"
|
||||
:key="status.id"
|
||||
:statusoid="status"
|
||||
:isPreview="true"
|
||||
:is-preview="true"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
:bound-to="{ x: 'container' }"
|
||||
@show="enter"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<slot />
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<Status
|
||||
v-if="status"
|
||||
:is-preview="true"
|
||||
|
||||
@@ -7,12 +7,20 @@ const StillImage = {
|
||||
'imageLoadHandler',
|
||||
'alt',
|
||||
'height',
|
||||
'width'
|
||||
'width',
|
||||
'noStopGifs'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||
stopGifs:
|
||||
!this.noStopGifs
|
||||
&& (
|
||||
this.$store.getters.mergedConfig.stopGifs
|
||||
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
),
|
||||
isAnimated: false,
|
||||
isPixelArt: false,
|
||||
imageTypeLabel: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -33,33 +41,38 @@ const StillImage = {
|
||||
if (!image) return
|
||||
this.imageLoadHandler && this.imageLoadHandler(image)
|
||||
this.detectAnimation(image)
|
||||
this.detectPixelArt(image)
|
||||
this.drawThumbnail()
|
||||
},
|
||||
onError () {
|
||||
this.imageLoadError && this.imageLoadError()
|
||||
},
|
||||
detectPixelArt (image) {
|
||||
// Safe maximum: 32x32 image, equivalent or smaller
|
||||
this.isPixelArt ||= image.naturalHeight * image.naturalWidth <= 32 * 32;
|
||||
// Common size for oldweb badges.
|
||||
this.isPixelArt ||= image.naturalWidth == 88 && image.naturalHeight == 31;
|
||||
console.log(image.src+" is "+image.naturalHeight+"x"+image.naturalWidth+" - "+(this.isPixelArt ? "pixel art" : "normal"))
|
||||
},
|
||||
detectAnimation (image) {
|
||||
// If there are no file extensions, the mimetype isn't set, and no mediaproxy is available, we can't figure out
|
||||
// the mimetype of the image.
|
||||
const hasFileExtension = this.src.split('/').pop().includes('.') // TODO: Better check?
|
||||
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
||||
if (!hasFileExtension && this.mimetype === undefined && !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)
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
|
||||
this.isAnimated = true
|
||||
return
|
||||
this.detectWithoutMediaProxy(image)
|
||||
} else {
|
||||
this.detectWithMediaProxy(image)
|
||||
}
|
||||
// harmless CORS errors without-- clean console with
|
||||
if (!mediaProxyAvailable) return
|
||||
// Animated JPEGs?
|
||||
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
|
||||
fetch(image.src, {
|
||||
referrerPolicy: 'same-origin'
|
||||
@@ -68,12 +81,20 @@ const StillImage = {
|
||||
// We don't need to read the whole file so only call it once
|
||||
data.body.getReader().read()
|
||||
.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.setLabel('GIF')
|
||||
return
|
||||
}
|
||||
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
|
||||
if (this.isAnimatedWEBP(reader.value)) {
|
||||
this.isAnimated = true
|
||||
this.setLabel('WEBP')
|
||||
return
|
||||
}
|
||||
if (this.isAnimatedPNG(reader.value)) {
|
||||
this.isAnimated = true
|
||||
this.setLabel('APNG')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -81,6 +102,53 @@ const StillImage = {
|
||||
// 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) {
|
||||
/**
|
||||
* WEBP HEADER CHUNK
|
||||
@@ -115,14 +183,53 @@ const StillImage = {
|
||||
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
||||
},
|
||||
drawThumbnail() {
|
||||
const canvas = this.$refs.canvas
|
||||
if (!this.$refs.canvas) return
|
||||
const image = this.$refs.src
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
||||
const canvas = this.$refs.canvas;
|
||||
if (!canvas) return;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
const image = this.$refs.src;
|
||||
const parentElement = canvas.parentElement;
|
||||
|
||||
// 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 = !this.isPixelArt;
|
||||
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 () {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
ref="still-image"
|
||||
class="still-image"
|
||||
:class="{ animated: animated }"
|
||||
:class="{ animated: animated, pixelart: isPixelArt }"
|
||||
:style="style"
|
||||
>
|
||||
<div
|
||||
v-if="animated && imageTypeLabel"
|
||||
class="image-type-label"
|
||||
>
|
||||
{{ imageTypeLabel }}
|
||||
</div>
|
||||
<canvas
|
||||
v-if="animated"
|
||||
ref="canvas"
|
||||
@@ -57,30 +64,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.animated {
|
||||
&::before {
|
||||
zoom: var(--_still_image-label-scale, 1);
|
||||
content: 'gif';
|
||||
.image-type-label {
|
||||
position: absolute;
|
||||
top: 0.25em;
|
||||
left: 0.25em;
|
||||
line-height: 1;
|
||||
font-size: 0.7em;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
font-size: 0.6em;
|
||||
background: rgba(127, 127, 127, 0.5);
|
||||
color: #fff;
|
||||
display: block;
|
||||
padding: 2px 4px;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
z-index: 2;
|
||||
visibility: var(--_still-image-label-visibility, visible);
|
||||
}
|
||||
|
||||
&.animated {
|
||||
&:hover canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
&:hover .image-type-label {
|
||||
visibility: var(--_still-image-label-visibility, hidden);
|
||||
}
|
||||
|
||||
@@ -92,5 +95,8 @@
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.pixelart {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
:dive="dive ? () => dive(status.id) : undefined"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
/>
|
||||
<div
|
||||
v-if="currentReplies.length && threadShowing"
|
||||
|
||||
@@ -28,4 +28,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeline {
|
||||
min-height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="TimelineQuickSettings"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<div v-if="loggedIn">
|
||||
<button
|
||||
@@ -80,7 +80,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled">
|
||||
<FAIcon icon="filter" />
|
||||
</button>
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
@show="openMenu"
|
||||
@close="() => isOpen = false"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="timeline-menu-popover popover-default">
|
||||
<TimelineMenuContent />
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled title timeline-menu-title">
|
||||
<span class="timeline-title">{{ timelineName() }}</span>
|
||||
<span>
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
:title="$t('nav.twkn_timeline_description')"
|
||||
:aria-label="$t('nav.twkn_timeline_description')"
|
||||
>{{ $t("nav.twkn") }}</span>
|
||||
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser">
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="publicTimelineVisible"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="publicTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
@@ -40,9 +40,9 @@
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federatedTimelineVisible"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
v-if="federatedTimelineVisible"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user