Compare commits

..

No commits in common. "bnakkoma" and "develop" have entirely different histories.

189 changed files with 9108 additions and 10215 deletions

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules/
dist/
npm-debug.log
test/unit/coverage
test/e2e/reports
selenium-debug.log
.idea/
config/local.json
config/local.*.json
docs/site/
.vscode/
akkoma-fe.zip

2
.eslintignore Normal file
View file

@ -0,0 +1,2 @@
build/*.js
config/*.js

30
.eslintrc.js Normal file
View file

@ -0,0 +1,30 @@
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
}
}

3
.gitignore vendored
View file

@ -9,5 +9,4 @@ selenium-debug.log
config/local.json config/local.json
config/local.*.json config/local.*.json
docs/site/ docs/site/
.vscode/ .vscode/
akkoma-fe.zip

1
.node-version Normal file
View file

@ -0,0 +1 @@
7.2.1

View file

@ -1 +0,0 @@
nodejs 20.12.2

View file

@ -1,25 +1,24 @@
labels: platform: linux/amd64
platform: linux/amd64 pipeline:
steps:
lint: lint:
when: when:
event: event:
- pull_request - pull_request
image: node:20 image: node:18
commands: commands:
- yarn - yarn
- yarn lint - yarn lint
#- yarn stylelint
test: test:
when: when:
event: event:
- pull_request - pull_request
image: node:20 image: node:18
commands: commands:
- apt update - apt update
- apt install firefox-esr -y --no-install-recommends - apt install firefox-esr -y --no-install-recommends
- yarn - yarn
- yarn unit - yarn unit
build: build:
@ -29,7 +28,7 @@ steps:
branch: branch:
- develop - develop
- stable - stable
image: node:20 image: node:18
commands: commands:
- yarn - yarn
- yarn build - yarn build
@ -41,15 +40,15 @@ steps:
branch: branch:
- develop - develop
- stable - stable
image: node:20 image: node:18
secrets: secrets:
- SCW_ACCESS_KEY - SCW_ACCESS_KEY
- SCW_SECRET_KEY - SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID - SCW_DEFAULT_ORGANIZATION_ID
commands: commands:
- apt-get update && apt-get install -y rclone wget zip - apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64 - wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli - mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli - chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone - ./scaleway-cli object config install type=rclone
- zip akkoma-fe.zip -r dist - zip akkoma-fe.zip -r dist
@ -71,8 +70,8 @@ steps:
- SCW_DEFAULT_ORGANIZATION_ID - SCW_DEFAULT_ORGANIZATION_ID
commands: commands:
- apt-get update && apt-get install -y rclone wget git zip - apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64 - wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli - mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli - chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone - ./scaleway-cli object config install type=rclone
- cd docs - cd docs
@ -80,4 +79,4 @@ steps:
- mkdocs build - mkdocs build
- zip -r docs.zip site/* - zip -r docs.zip site/*
- cd site - cd site
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/ - rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/

View file

@ -1,24 +0,0 @@
FROM node:22.6.0-alpine3.20 as build
RUN apk add --no-cache \
git \
chromium-chromedriver
# use chromedriver from apk
ENV CHROMEDRIVER_FILEPATH=/usr/bin/chromedriver
WORKDIR /app/
ARG NODE_ENV=production
ENV YARN_CACHE_FOLDER=/.yarn/
COPY . /app/
RUN \
--mount=type=cache,target=/.yarn \
NODE_ENV=development yarn install \
&& NODE_ENV=${NODE_ENV} yarn run build
FROM scratch as result
COPY --from=build /app/dist/ /

View file

@ -1,26 +0,0 @@
RUNTIME ?= docker
BUILD_DIR ?= ./dist/
OUTFILE_ZIP ?= ./akkoma-fe.zip
NODE_ENV ?= production
.PHONY: all
all: build-fe package
.PHONY: build-fe
build-fe:
ifeq ("$(wildcard $(BUILD_DIR))","")
mkdir $(BUILD_DIR)
else
rm -rf $(BUILD_DIR)
mkdir $(BUILD_DIR)
endif
$(RUNTIME) build --build-arg NODE_ENV=$(NODE_ENV) --output type=local,dest=./dist/ .
.PHONY: package
package:
ifneq ("$(wildcard $(OUTFILE_ZIP))","")
rm $(OUTFILE_ZIP)
endif
zip -r -9 $(OUTFILE_ZIP) $(BUILD_DIR)
# vim:set noexpandtab:

View file

@ -1,76 +1,28 @@
# BNAkkoma: Brand New Akkoma # Akkoma-FE
<small>It's not that new. This is just a cheap pun on the title of a [furry anime](https://en.wikipedia.org/wiki/BNA:_Brand_New_Animal).</small> ![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
Also keep in mind that B in BNAkkoma stands for 'bleeding-edge', **features can be added, changed or removed at any time!**
If you are experiencing any strange quirks, make sure both your frontend and backend are up to date.
If your software are up to date but the bug is still there, ping me on the fediverse at '@itepechi@fedi.itepechi.me'.
## What is this?
This is a fork of [AkkomaGang/akkoma-fe](https://akkoma.dev/AkkomaGang/akkoma-fe/), with a custom theme and Makefile.
The differences from the upstream repository are described below:
- Added a Makefile where you can build the client inside a Docker container
- Refactored some components and styles
- Fixed a lot of broken CSS rules and misaligned elements
- Added and changed the default theme to a custom one
- Added support for setting the default post language
- Stole some commits from the original Pleroma frontend
- Added OpenSearch protocol support
- Added the Media tab to the search page (requires appropriate backend)
- Added 'Limit to Following' filter to the search page (requires appropriate backend)
- Added 'Limit to Local' filter to the search page (requires appropriate backend)
- Implemented lazy loading of search results
- More than 200 (!) Japanese translations have been fixed
- Removed some themes to save network bandwidth
- Improved PWA support, sort of
Although this frontend application is designed to work with the BNAkkoma backend, it remains compatible with the original version of Akkoma by only enabling additional features when the backend server returns a corresponding flag.
### How to build
**Requires 2GB+ memory.**
```sh
# Docker
make
# Podman
make RUNTIME=podman
```
---
## Akkoma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet?style=for-the-badge) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet?style=for-the-badge)
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as: This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions - Custom emoji reactions
## For Translators # For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE. The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there. Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
## FOR ADMINS # FOR ADMINS
To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc. To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
### Build Setup ## Build Setup
```bash ``` bash
# install dependencies # install dependencies
corepack enable npm install -g yarn
yarn yarn
# serve with hot reload at localhost:8080 # serve with hot reload at localhost:8080
@ -83,21 +35,21 @@ npm run build
npm run unit npm run unit
``` ```
## For Contributors: # For Contributors:
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: You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
- `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases. * `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
- `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode. * `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE. FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
## Configuration # Configuration
Edit config.json for configuration. Edit config.json for configuration.
### Options ## Options
#### Login methods ### Login methods
`loginMethod` can be set to either `password` (the default) or `token`, which will use the full oauth redirection flow, which is useful for SSO situations. ```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -1,36 +1,36 @@
// https://github.com/shelljs/shelljs // https://github.com/shelljs/shelljs
require("./check-versions")(); require('./check-versions')()
require("shelljs/global"); require('shelljs/global')
env.NODE_ENV = "production"; env.NODE_ENV = 'production'
var path = require("path"); var path = require('path')
var config = require("../config"); var config = require('../config')
var webpack = require("webpack"); var ora = require('ora')
var webpackConfig = require("./webpack.prod.conf"); var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')
console.log( console.log(
" Tip:\n" + ' Tip:\n' +
" Built files are meant to be served over an HTTP server.\n" + ' Built files are meant to be served over an HTTP server.\n' +
" Opening index.html over file:// won't work.\n", ' Opening index.html over file:// won\'t work.\n'
); )
var assetsPath = path.join( var spinner = ora('building for production...')
config.build.assetsRoot, spinner.start()
config.build.assetsSubDirectory,
); var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm("-rf", assetsPath); rm('-rf', assetsPath)
mkdir("-p", assetsPath); mkdir('-p', assetsPath)
cp("-R", "static/*", assetsPath); cp('-R', 'static/*', assetsPath)
webpack(webpackConfig, function (err, stats) { webpack(webpackConfig, function (err, stats) {
if (err) throw err; spinner.stop()
process.stdout.write( if (err) throw err
stats.toString({ process.stdout.write(stats.toString({
colors: true, colors: true,
modules: false, modules: false,
children: false, children: false,
chunks: false, chunks: false,
chunkModules: false, chunkModules: false
}) + "\n", }) + '\n')
); })
});

View file

@ -5,7 +5,7 @@ var path = require('path')
var express = require('express') var express = require('express')
var webpack = require('webpack') var webpack = require('webpack')
var opn = require('opn') var opn = require('opn')
const { createProxyMiddleware } = require('http-proxy-middleware'); var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing' var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf') ? require('./webpack.prod.conf')
: require('./webpack.dev.conf') : require('./webpack.dev.conf')
@ -36,13 +36,7 @@ Object.keys(proxyTable).forEach(function (context) {
if (typeof options === 'string') { if (typeof options === 'string') {
options = { target: options } options = { target: options }
} }
const targetUrl = new URL(options.target); app.use(proxyMiddleware(context, options))
// add path
targetUrl.pathname = context;
options.target = targetUrl.toString();
console.log("Proxying", context, "to", options.target);
app.use(context, createProxyMiddleware(options))
}) })
// handle fallback for HTML5 history API // handle fallback for HTML5 history API

View file

@ -3,7 +3,6 @@ var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../') var projectRoot = path.resolve(__dirname, '../')
var { VueLoaderPlugin } = require('vue-loader') var { VueLoaderPlugin } = require('vue-loader')
const ESLintPlugin = require('eslint-webpack-plugin');
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the // check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -36,7 +35,6 @@ module.exports = {
], ],
fallback: { fallback: {
"url": require.resolve("url/"), "url": require.resolve("url/"),
querystring: require.resolve("querystring-es3")
}, },
alias: { alias: {
'static': path.resolve(__dirname, '../static'), 'static': path.resolve(__dirname, '../static'),
@ -49,6 +47,20 @@ module.exports = {
module: { module: {
noParse: /node_modules\/localforage\/dist\/localforage.js/, noParse: /node_modules\/localforage\/dist\/localforage.js/,
rules: [ rules: [
{
enforce: 'pre',
test: /\.(js|vue)$/,
include: projectRoot,
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter'),
sourceMap: config.build.productionSourceMap,
extract: true
}
}
},
{ {
enforce: 'post', enforce: 'post',
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
@ -106,9 +118,6 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new VueLoaderPlugin(), new VueLoaderPlugin()
new ESLintPlugin({
configType: 'flat'
})
] ]
} }

View file

@ -1,4 +1,4 @@
{ {
"target": "https://otp.akkoma.dev/", "target": "https://pleroma.soykaf.com/",
"staticConfigPreference": false "staticConfigPreference": false
} }

View file

@ -2,4 +2,5 @@ var { merge } = require('webpack-merge')
var devEnv = require('./dev.env') var devEnv = require('./dev.env')
module.exports = merge(devEnv, { module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
}) })

View file

@ -1,31 +0,0 @@
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'
]
}
]

View file

@ -11,7 +11,6 @@
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder"> <link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta--> <!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
</head> </head>
<body class="hidden"> <body class="hidden">

View file

@ -1,7 +1,7 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "3.10.0+bnakkoma", "version": "3.10.0",
"description": "An extended frontend for Akkoma instances", "description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>", "author": "Roger Braun <roger@rogerbraun.net>",
"private": true, "private": true,
"scripts": { "scripts": {
@ -12,118 +12,120 @@
"e2e": "node test/e2e/runner.js", "e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e", "test": "npm run unit && npm run e2e",
"stylelint": "stylelint src/**/*.scss", "stylelint": "stylelint src/**/*.scss",
"lint": "eslint src test/unit/specs test/e2e/specs", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs" "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.17.8", "@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "^2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@floatingghost/pinch-zoom-element": "^1.3.1", "@floatingghost/pinch-zoom-element": "^1.3.1",
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "^3.0.8", "@fortawesome/vue-fontawesome": "3.0.1",
"@vuelidate/core": "^2.0.3", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.4", "@vuelidate/validators": "^2.0.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.4",
"body-scroll-lock": "^3.1.5", "body-scroll-lock": "2.7.1",
"chromatism": "^3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "^4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "^1.6.2", "cropperjs": "1.5.12",
"diff": "^5.2.0", "diff": "3.5.0",
"escape-html": "^1.0.3", "escape-html": "1.0.3",
"iso-639-1": "^2.1.15", "iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"localforage": "^1.10.0", "localforage": "1.10.0",
"parse-link-header": "^2.0.0", "parse-link-header": "^2.0.0",
"phoenix": "^1.7.12", "phoenix": "1.6.2",
"punycode.js": "^2.3.1", "punycode.js": "2.1.0",
"qrcode": "^1.5.3", "qrcode": "1",
"querystring-es3": "^0.2.1", "url": "^0.11.0",
"url": "^0.11.3", "vue": "^3.2.31",
"vue": "^3.4.38", "vue-i18n": "^9.2.2",
"vue-i18n": "^9.14.0", "vue-router": "4.0.14",
"vue-router": "^4.4.3", "vue-template-compiler": "2.6.11",
"vue-template-compiler": "^2.7.16", "vuex": "4.0.2"
"vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.6", "@babel/core": "7.17.8",
"@babel/eslint-parser": "^7.19.1", "@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "^7.24.6", "@babel/plugin-transform-runtime": "7.17.0",
"@babel/preset-env": "^7.24.6", "@babel/preset-env": "7.16.11",
"@babel/register": "^7.24.6", "@babel/register": "7.17.7",
"@intlify/vue-i18n-loader": "^5.0.0", "@intlify/vue-i18n-loader": "^5.0.0",
"@ungap/event-target": "^0.2.4", "@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
"@vue/babel-plugin-jsx": "^1.2.2", "@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "^3.1.0", "@vue/compiler-sfc": "^3.1.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"autoprefixer": "^10.4.19", "autoprefixer": "6.7.7",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.0",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "^4.3.7", "chai": "^4.3.7",
"chalk": "^1.1.3", "chalk": "1.1.3",
"chromedriver": "^119.0.1", "chromedriver": "^107.0.3",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"css-loader": "^7.1.2", "css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"eslint": "^9.3.0", "eslint": "^7.32.0",
"eslint-config-standard": "^17.1.0", "eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-import": "^2.29.1", "eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.2.0", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^9.26.0", "eslint-plugin-vue": "^9.7.0",
"eslint-webpack-plugin": "^4.2.0", "eventsource-polyfill": "0.9.6",
"eventsource-polyfill": "^0.9.6", "express": "4.17.3",
"express": "^4.19.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"function-bind": "^1.1.2", "function-bind": "1.1.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "^3.0.0", "http-proxy-middleware": "0.21.0",
"json-loader": "^0.5.7", "inject-loader": "2.0.1",
"karma": "^6.4.3", "isparta-loader": "2.0.0",
"karma-coverage": "^2.2.1", "json-loader": "0.5.7",
"karma-firefox-launcher": "^2.1.3", "karma": "6.3.17",
"karma-mocha": "^2.0.1", "karma-coverage": "1.1.2",
"karma-mocha-reporter": "^2.2.5", "karma-firefox-launcher": "1.3.0",
"karma-sinon-chai": "^2.0.2", "karma-mocha": "2.0.1",
"karma-sourcemap-loader": "^0.4.0", "karma-mocha-reporter": "2.2.5",
"karma-spec-reporter": "^0.0.36", "karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33",
"karma-webpack": "^5.0.0", "karma-webpack": "^5.0.0",
"lodash": "^4.17.21", "lodash": "4.17.21",
"lolex": "^6.0.0", "lolex": "1.6.0",
"mini-css-extract-plugin": "^2.9.0", "mini-css-extract-plugin": "0.12.0",
"mocha": "^10.4.0", "mocha": "3.5.3",
"nightwatch": "^3.6.3", "nightwatch": "0.9.21",
"opn": "^6.0.0", "opn": "4.0.2",
"ora": "0.4.1",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "^8.1.1", "postcss-loader": "3.0.0",
"postcss-sass": "^0.5.0", "postcss-sass": "^0.5.0",
"raw-loader": "^4.0.2", "raw-loader": "0.5.1",
"sass": "^1.77.2", "sass": "^1.56.0",
"sass-loader": "^14.2.1", "sass-loader": "^13.2.0",
"selenium-server": "^3.141.59", "selenium-server": "2.53.1",
"semver": "^7.6.2", "semver": "5.7.1",
"shelljs": "^0.8.5", "shelljs": "0.8.5",
"sinon": "^18.0.0", "sinon": "2.4.1",
"sinon-chai": "^3.7.0", "sinon-chai": "2.14.0",
"stylelint": "^14.15.0", "stylelint": "^14.15.0",
"stylelint-config-recommended-vue": "^1.4.0", "stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^29.0.0", "stylelint-config-standard": "^29.0.0",
"stylelint-config-standard-scss": "^6.1.0", "stylelint-config-standard-scss": "^6.1.0",
"stylelint-rscss": "^0.4.0", "stylelint-rscss": "^0.4.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.0.0",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.2",
"webpack": "^5.91.0", "webpack": "^5.75.0",
"webpack-dev-middleware": "^7.2.1", "webpack-dev-middleware": "^5.3.3",
"webpack-hot-middleware": "^2.26.1", "webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.10.0", "webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^7.1.0" "workbox-webpack-plugin": "^6.5.4"
}, },
"engines": { "engines": {
"node": ">= 16.0.0", "node": ">= 16.0.0",

View file

@ -64,11 +64,6 @@ export default {
'-' + this.layoutType '-' + this.layoutType
] ]
}, },
pageBackground () {
return this.mergedConfig.displayPageBackgrounds
? this.$store.state.users.displayBackground
: null
},
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image }, userBackground () { return this.currentUser.background_image },
instanceBackground () { instanceBackground () {
@ -76,7 +71,7 @@ export default {
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background () { return this.pageBackground || this.userBackground || this.instanceBackground }, background () { return this.userBackground || this.instanceBackground },
bgStyle () { bgStyle () {
if (this.background) { if (this.background) {
return { return {

View file

@ -8,7 +8,7 @@
} }
html { html {
font-size: 0.875rem; font-size: 14px;
// overflow-x: clip causes my browser's tab to crash with SIGILL lul // overflow-x: clip causes my browser's tab to crash with SIGILL lul
} }
@ -665,12 +665,8 @@ option {
} }
.alert { .alert {
display: inline-flex; margin: 0 0.35em;
justify-content: center; padding: 0 0.25em;
align-items: center;
line-height: 1;
margin: 0 0.5em;
padding: 0.35em;
border-radius: $fallback--tooltipRadius; border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
@ -771,8 +767,6 @@ option {
.btn.button-default { .btn.button-default {
min-height: 2em; min-height: 2em;
padding-left: 0.5em;
padding-right: 0.5em;
} }
.new-status-notification { .new-status-notification {
@ -786,11 +780,6 @@ option {
.mobile-hidden { .mobile-hidden {
display: none; display: none;
} }
.btn.button-default {
padding-left: 0.7em;
padding-right: 0.7em;
}
} }
@keyframes spin { @keyframes spin {

View file

@ -77,9 +77,7 @@ const getInstanceConfig = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
// don't override cookie if set // don't override cookie if set
if (!Cookies.get('userLanguage')) { if (!Cookies.get('userLanguage')) {
const language = resolveLanguage(data.languages) store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
store.dispatch('setOption', { name: 'interfaceLanguage', value: language })
store.dispatch('setOption', { name: 'postLanguage', value: language })
} }
if (vapidPublicKey) { if (vapidPublicKey) {
@ -185,12 +183,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('renderMisskeyMarkdown') copyInstanceOption('renderMisskeyMarkdown')
copyInstanceOption('sidebarRight') copyInstanceOption('sidebarRight')
if (config.backendCommitUrl)
copyInstanceOption('backendCommitUrl')
if (config.frontendCommitUrl)
copyInstanceOption('frontendCommitUrl')
return store.dispatch('setTheme', config['theme']) return store.dispatch('setTheme', config['theme'])
} }
@ -287,10 +279,6 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') }) store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
store.dispatch('setInstanceOption', { name: 'searchTypeMediaEnabled', value: features.includes('bnakkoma:search_type_media') })
store.dispatch('setInstanceOption', { name: 'searchOptionFollowingEnabled', value: features.includes('bnakkoma:search_option_following') })
store.dispatch('setInstanceOption', { name: 'searchOptionLocalEnabled', value: features.includes('bnakkoma:search_option_local') })
store.dispatch('setInstanceOption', { name: 'opensearchProtocolSupported', value: features.includes('bnakkoma:opensearch_protocol') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -414,16 +402,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
// Create a link tag for OpenSearch and forget about it
if (store.state.instance.opensearchProtocolSupported) {
const node = document.createElement('link')
node.setAttribute('rel', 'search')
node.setAttribute('type', 'application/opensearchdescription+xml')
node.setAttribute('href', '/opensearch.xml')
node.setAttribute('title', store.state.instance.name)
document.head.appendChild(node)
}
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: routes(store), routes: routes(store),

View file

@ -9,7 +9,7 @@
</div> </div>
</template> </template>
<script src="./about.js"></script> <script src="./about.js" ></script>
<style lang="scss"> <style lang="scss">
</style> </style>

View file

@ -6,7 +6,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <button
@ -71,7 +71,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="button-unstyled ellipsis-button"> <button class="button-unstyled ellipsis-button">
<FAIcon <FAIcon
class="icon" class="icon"
@ -93,7 +93,7 @@
keypath="user_card.block_confirm" keypath="user_card.block_confirm"
tag="span" tag="span"
> >
<template #user> <template v-slot:user>
<span <span
v-text="user.screen_name_ui" v-text="user.screen_name_ui"
/> />

View file

@ -37,7 +37,7 @@
white-space: pre-line; white-space: pre-line;
word-break: break-word; word-break: break-word;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: auto; overflow: scroll;
} }
&.-static { &.-static {

View file

@ -155,7 +155,6 @@
> >
<StillImage <StillImage
class="image" class="image"
:loading="'lazy'"
:referrerpolicy="referrerpolicy" :referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype" :mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
@ -247,8 +246,8 @@
ref="flash" ref="flash"
class="flash" class="flash"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
@player-opened="setFlashLoaded(true)" @playerOpened="setFlashLoaded(true)"
@player-closed="setFlashLoaded(false)" @playerClosed="setFlashLoaded(false)"
/> />
</span> </span>
</div> </div>

View file

@ -14,7 +14,7 @@
</div> </div>
</template> </template>
<script src="./avatar_list.js"></script> <script src="./avatar_list.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -61,11 +61,12 @@
} }
&-user-name { &-user-name {
--emoji-size: 14px; img {
} object-fit: contain;
height: 16px;
&-user-name-value { width: 16px;
font-weight: bold; vertical-align: middle;
}
} }
&-user-name-value, &-user-name-value,

View file

@ -22,36 +22,31 @@
<script> <script>
export default { export default {
emits: ['update:modelValue'],
props: [ props: [
'modelValue', 'modelValue',
'indeterminate', 'indeterminate',
'disabled' 'disabled'
], ]
emits: ['update:modelValue']
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
$checkbox-size: 1.2em;
$padding-size: 0.5em;
.checkbox { .checkbox {
position: relative; position: relative;
display: inline-block; display: inline-block;
min-height: $checkbox-size; min-height: 1.2em;
padding-left: calc($checkbox-size + $padding-size);
&-indicator { &-indicator {
position: relative; position: relative;
padding-left: calc($checkbox-size + $padding-size); padding-left: 1.2em;
margin-left: calc(($checkbox-size + $padding-size) * -1);
} }
&-indicator::before { &-indicator::before {
position: absolute; position: absolute;
left: 0; right: 0;
top: 0; top: 0;
display: block; display: block;
content: '✓'; content: '✓';
@ -97,6 +92,11 @@ $padding-size: 0.5em;
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
}
& > span {
margin-left: .5em;
} }
} }
</style> </style>

View file

@ -14,7 +14,7 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:model-value="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
@ -46,6 +46,7 @@
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" src="./color_input.scss"></style>
<script> <script>
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
@ -107,7 +108,6 @@ export default {
} }
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss"> <style lang="scss">
.color-control { .color-control {

View file

@ -25,8 +25,6 @@
</dialog-modal> </dialog-modal>
</template> </template>
<script src="./confirm_modal.js"></script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../_variables'; @import '../../_variables';
@ -37,3 +35,5 @@
} }
} }
</style> </style>
<script src="./confirm_modal.js"></script>

View file

@ -267,11 +267,11 @@ const conversation = {
}, },
replies () { replies () {
let i = 1 let i = 1
// eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => { return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) { if (irid) {
result[irid] = result[irid] || [] result[irid] = result[irid] || []
result[irid].push({ result[irid].push({

View file

@ -91,7 +91,7 @@
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)" :controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -184,7 +184,7 @@
:toggle-status-content-property="toggleStatusContentProperty" :toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
</div> </div>
</div> </div>

View file

@ -51,6 +51,7 @@ export default {
ConfirmModal ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && ( supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') || window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
@ -76,7 +77,8 @@ export default {
}, },
logoBgStyle () { logoBgStyle () {
return Object.assign({ return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0` 'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : { }, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent' 'background-color': this.enableMask ? '' : 'transparent'
}) })
@ -118,6 +120,9 @@ export default {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () { openSettingsModal () {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal')
}, },

View file

@ -56,6 +56,13 @@
.logo { .logo {
grid-area: logo; grid-area: logo;
position: relative; position: relative;
transition: opacity;
transition-timing-function: ease-out;
transition-duration: 100ms;
@media all and (min-width: 800px) {
opacity: 1 !important;
}
.mask { .mask {
mask-repeat: no-repeat; mask-repeat: no-repeat;

View file

@ -44,9 +44,9 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="publicTimelineVisible"
:to="{ name: 'public-timeline' }" :to="{ name: 'public-timeline' }"
class="nav-icon" class="nav-icon"
v-if="publicTimelineVisible"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -68,9 +68,9 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="federatedTimelineVisible"
:to="{ name: 'public-external-timeline' }" :to="{ name: 'public-external-timeline' }"
class="nav-icon" class="nav-icon"
v-if="federatedTimelineVisible"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -98,6 +98,7 @@
<div class="item right actions"> <div class="item right actions">
<search-bar <search-bar
v-if="currentUser || !privateMode" v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled"
@click.stop @click.stop
/> />
<div <div

View file

@ -9,7 +9,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template #progress> <template v-slot:progress>
{{ $t('domain_mute_card.unmute_progress') }} {{ $t('domain_mute_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -19,7 +19,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.mute') }} {{ $t('domain_mute_card.mute') }}
<template #progress> <template v-slot:progress>
{{ $t('domain_mute_card.mute_progress') }} {{ $t('domain_mute_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="isFormVisible" v-if="isFormVisible"
class="edit-form-modal-view" class="edit-form-modal-view"
@backdrop-clicked="closeModal" @backdropClicked="closeModal"
> >
<div class="edit-form-modal-panel panel"> <div class="edit-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
@ -11,10 +11,10 @@
<PostStatusForm <PostStatusForm
class="panel-body" class="panel-body"
v-bind="params" v-bind="params"
:disable-polls="true"
:disable-visibility-selector="true"
:post-handler="doEditStatus"
@posted="closeModal" @posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus"
/> />
</div> </div>
</Modal> </Modal>

View file

@ -43,10 +43,7 @@
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span <span v-if="!suggestion.mfm" class="image">
v-if="!suggestion.mfm"
class="image"
>
<img <img
v-if="suggestion.img" v-if="suggestion.img"
:src="suggestion.img" :src="suggestion.img"

View file

@ -122,14 +122,14 @@ export const suggestUsers = ({ dispatch, state }) => {
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
displayText: screen_name_ui, displayText: screen_name_ui,
detailText: name, detailText: name,
imageUrl: profile_image_url_original, imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' ' replacement: '@' + screen_name + ' '
})) }))
/* eslint-enable camelcase */
suggestions = newSuggestions || [] suggestions = newSuggestions || []
return suggestions return suggestions

View file

@ -42,7 +42,7 @@
</div> </div>
</template> </template>
<script src="./emoji_reactions.js"></script> <script src="./emoji_reactions.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template #content="{close}"> <template v-slot:content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -172,7 +172,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="button-unstyled popover-trigger"> <button class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
@ -205,7 +205,7 @@
</Popover> </Popover>
</template> </template>
<script src="./extra_buttons.js"></script> <script src="./extra_buttons.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -35,7 +35,7 @@
</div> </div>
</template> </template>
<script src="./favorite_button.js"></script> <script src="./favorite_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -23,7 +23,7 @@
</div> </div>
</template> </template>
<script src="./features_panel.js"></script> <script src="./features_panel.js" ></script>
<style lang="scss"> <style lang="scss">
.features-panel li { .features-panel li {

View file

@ -1,8 +1,5 @@
<template> <template>
<basic-user-card <basic-user-card :user="user" v-if="show">
v-if="show"
:user="user"
>
<div class="follow-request-card-content-container"> <div class="follow-request-card-content-container">
<button <button
class="btn button-default" class="btn button-default"

View file

@ -47,7 +47,7 @@
</div> </div>
</template> </template>
<script src="./font_control.js"></script> <script src="./font_control.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -31,8 +31,8 @@
:description="descriptions && descriptions[attachment.id]" :description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong" :hide-description="size === 'small' || tooManyAttachments && hidingLong"
:style="itemStyle(attachment.id, row.items)" :style="itemStyle(attachment.id, row.items)"
@set-media="onMedia" @setMedia="onMedia"
@natural-size-load="onNaturalSizeLoad" @naturalSizeLoad="onNaturalSizeLoad"
/> />
</div> </div>
</div> </div>

View file

@ -14,6 +14,6 @@
</span> </span>
</template> </template>
<script src="./hashtag_link.js" /> <script src="./hashtag_link.js"/>
<style lang="scss" src="./hashtag_link.scss" /> <style lang="scss" src="./hashtag_link.scss"/>

View file

@ -10,4 +10,4 @@
</div> </div>
</template> </template>
<script src="./instance_specific_panel.js"></script> <script src="./instance_specific_panel.js" ></script>

View file

@ -42,7 +42,6 @@ export default {
@import '../../_variables.scss'; @import '../../_variables.scss';
.list { .list {
min-height: 1em;
&-item:not(:last-child) { &-item:not(:last-child) {
border-bottom: 1px solid; border-bottom: 1px solid;
border-bottom-color: $fallback--border; border-bottom-color: $fallback--border;

View file

@ -10,7 +10,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>{{ $t("about.bubble_instances_description") }}:</p> <p>{{ $t("about.bubble_instances_description")}}:</p>
<ul> <ul>
<li <li
v-for="instance in bubbleInstances" v-for="instance in bubbleInstances"

View file

@ -90,7 +90,7 @@
</div> </div>
</template> </template>
<script src="./login_form.js"></script> <script src="./login_form.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="showing" v-if="showing"
class="media-modal-view" class="media-modal-view"
@backdrop-clicked="hideIfNotSwiped" @backdropClicked="hideIfNotSwiped"
> >
<SwipeClick <SwipeClick
v-if="type === 'image'" v-if="type === 'image'"

View file

@ -42,7 +42,7 @@ const mediaUpload = {
.then((fileData) => { .then((fileData) => {
self.$emit('uploaded', fileData) self.$emit('uploaded', fileData)
self.decreaseUploadCount() self.decreaseUploadCount()
}, (error) => { }, (error) => { // eslint-disable-line handle-callback-err
self.$emit('upload-failed', 'default') self.$emit('upload-failed', 'default')
self.decreaseUploadCount() self.decreaseUploadCount()
}) })

View file

@ -26,7 +26,7 @@
</label> </label>
</template> </template>
<script src="./media_upload.js"></script> <script src="./media_upload.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -93,6 +93,9 @@ const MentionLink = {
this.highlightType this.highlightType
] ]
}, },
useAtIcon () {
return this.mergedConfig.useAtIcon
},
isRemote () { isRemote () {
return this.userName !== this.userNameFull return this.userName !== this.userNameFull
}, },

View file

@ -66,6 +66,6 @@
</span> </span>
</template> </template>
<script src="./mention_link.js" /> <script src="./mention_link.js"/>
<style lang="scss" src="./mention_link.scss" /> <style lang="scss" src="./mention_link.scss"/>

View file

@ -37,5 +37,5 @@
</span> </span>
</span> </span>
</template> </template>
<script src="./mentions_line.js"></script> <script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" /> <style lang="scss" src="./mentions_line.scss" />

View file

@ -69,4 +69,4 @@
</div> </div>
</div> </div>
</template> </template>
<script src="./recovery_form.js"></script> <script src="./recovery_form.js" ></script>

View file

@ -18,7 +18,6 @@
<input <input
id="code" id="code"
v-model="code" v-model="code"
autocomplete="one-time-code"
class="form-control" class="form-control"
> >
</div> </div>

View file

@ -5,7 +5,6 @@
class="mod-modal" class="mod-modal"
:class="{ peek: modalPeeked }" :class="{ peek: modalPeeked }"
:no-background="modalPeeked" :no-background="modalPeeked"
@backdropClicked="closeModal"
> >
<div class="mod-modal-panel panel"> <div class="mod-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">

View file

@ -4,7 +4,7 @@
class="panel-heading" class="panel-heading"
@click="toggleHidden" @click="toggleHidden"
> >
<h4>{{ $t('moderation.reports.report') + ' ' + account.screen_name }}</h4> <h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
<button <button
v-if="isOpen" v-if="isOpen"
class="button-default" class="button-default"
@ -24,7 +24,7 @@
class="button-default" class="button-default"
@click.stop="updateReportState('open')" @click.stop="updateReportState('open')"
> >
{{ $t('moderation.reports.reopen') }} {{ $t('moderation.reports.reopen') }}
</button> </button>
</div> </div>
<div <div
@ -35,10 +35,7 @@
<div v-if="content"> <div v-if="content">
{{ decode(content) }} {{ decode(content) }}
</div> </div>
<i <i v-else class="faint">
v-else
class="faint"
>
{{ $t('moderation.reports.no_content') }} {{ $t('moderation.reports.no_content') }}
</i> </i>
<div class="report-author"> <div class="report-author">
@ -46,12 +43,12 @@
class="small-avatar" class="small-avatar"
:user="actor" :user="actor"
/> />
{{ actor.screen_name }} {{ this.actor.screen_name }}
</div> </div>
</div> </div>
<div <div
v-if="!hidden && statuses.length > 0"
class="dropdown" class="dropdown"
v-if="!hidden && this.statuses.length > 0"
> >
<button <button
class="button button-unstyled dropdown-header" class="button button-unstyled dropdown-header"
@ -77,8 +74,8 @@
</div> </div>
</div> </div>
<div <div
v-if="!hidden && notes.length > 0"
class="dropdown" class="dropdown"
v-if="!hidden && this.notes.length > 0"
> >
<button <button
class="button button-unstyled dropdown-header" class="button button-unstyled dropdown-header"
@ -102,9 +99,9 @@
</div> </div>
<div class="report-add-note"> <div class="report-add-note">
<textarea <textarea
v-model.trim="note"
rows="1" rows="1"
cols="1" cols="1"
v-model.trim="note"
:placeholder="$t('moderation.reports.note_placeholder')" :placeholder="$t('moderation.reports.note_placeholder')"
/> />
<button <button
@ -137,7 +134,7 @@
:offset="{ y: 5 }" :offset="{ y: 5 }"
remove-padding remove-padding
> >
<template #trigger> <template v-slot:trigger>
<button <button
class="btn button-default" class="btn button-default"
:disabled="!tagPolicyEnabled" :disabled="!tagPolicyEnabled"
@ -150,7 +147,7 @@
/> />
</button> </button>
</template> </template>
<template #content="{close}"> <template v-slot:content="{close}">
<div <div
class="dropdown-menu" class="dropdown-menu"
:disabled="!tagPolicyEnabled" :disabled="!tagPolicyEnabled"

View file

@ -6,7 +6,7 @@
class="small-avatar" class="small-avatar"
:user="user" :user="user"
/> />
{{ user.screen_name }} {{ this.user.screen_name }}
</div> </div>
<div class="header-right"> <div class="header-right">
<Timeago <Timeago

View file

@ -22,9 +22,6 @@ export default {
default: false default: false
} }
}, },
emits: [
'backdropClicked',
],
computed: { computed: {
classes () { classes () {
return { return {

View file

@ -8,7 +8,7 @@
@show="setToggled(true)" @show="setToggled(true)"
@close="setToggled(false)" @close="setToggled(false)"
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
@ -122,7 +122,7 @@
</span> </span>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button <button
class="btn button-default btn-block moderation-tools-button" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled }"
@ -137,11 +137,11 @@
v-if="showDeleteUserDialog" v-if="showDeleteUserDialog"
:on-cancel="deleteUserDialog.bind(this, false)" :on-cancel="deleteUserDialog.bind(this, false)"
> >
<template #header> <template v-slot:header>
{{ $t('user_card.admin_menu.delete_user') }} {{ $t('user_card.admin_menu.delete_user') }}
</template> </template>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template #footer> <template v-slot:footer>
<button <button
class="btn button-default" class="btn button-default"
@click="deleteUserDialog(false)" @click="deleteUserDialog(false)"

View file

@ -102,7 +102,7 @@
</div> </div>
</template> </template>
<script src="./nav_panel.js"></script> <script src="./nav_panel.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -125,6 +125,20 @@
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
padding: 0; padding: 0;
&:first-child .menu-item {
border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
}
&:last-child .menu-item {
border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius;
border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
}
} }
li:last-child { li:last-child {

View file

@ -6,7 +6,6 @@ import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import StillImage from '../still-image/still-image.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -51,8 +50,7 @@ const Notification = {
Timeago, Timeago,
Status, Status,
RichContent, RichContent,
ConfirmModal, ConfirmModal
StillImage
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -116,13 +116,12 @@
scope="global" scope="global"
keypath="notifications.reacted_with" keypath="notifications.reacted_with"
> >
<still-image <img
v-if="notification.emoji_url !== null" v-if="notification.emoji_url !== null"
class="notification-reaction-emoji" class="notification-reaction-emoji"
:src="notification.emoji_url" :src="notification.emoji_url"
:title="notification.emoji" :name="notification.emoji"
:alt="notification.emoji" >
/>
<span <span
v-else v-else
class="emoji-reaction-emoji" class="emoji-reaction-emoji"
@ -152,6 +151,7 @@
> >
<Timeago <Timeago
:time="notification.created_at" :time="notification.created_at"
:with-direction="true"
:auto-update="240" :auto-update="240"
/> />
</router-link> </router-link>

View file

@ -5,7 +5,7 @@
placement="bottom" placement="bottom"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@ -72,7 +72,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="filter-trigger-button button-unstyled"> <button class="filter-trigger-button button-unstyled">
<FAIcon icon="filter" /> <FAIcon icon="filter" />
</button> </button>

View file

@ -105,12 +105,9 @@
flex: 1; flex: 1;
padding-left: 0.8em; padding-left: 0.8em;
min-width: 0; min-width: 0;
}
.heading-right, .notification-right {
.timeago { .timeago {
display: inline-block; min-width: 3em;
min-width: 6em;
text-align: right; text-align: right;
} }
} }

View file

@ -14,7 +14,7 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:model-value="$emit('update:modelValue', !present ? fallback : undefined)" @update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
/> />
<input <input
:id="name" :id="name"

View file

@ -2,6 +2,7 @@
<pinch-zoom <pinch-zoom
class="pinch-zoom-parent" class="pinch-zoom-parent"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners"
> >
<slot /> <slot />
</pinch-zoom> </pinch-zoom>

View file

@ -18,9 +18,7 @@
:placeholder="$t('polls.option')" :placeholder="$t('polls.option')"
:maxlength="maxLength" :maxlength="maxLength"
@change="updatePollToParent" @change="updatePollToParent"
@keydown.enter.stop.prevent=" @keydown.enter.stop.prevent="nextOption(index)"
$event.isComposing || $event.keyCode === 229 || nextOption(index)
"
> >
</div> </div>
<button <button

View file

@ -3,14 +3,14 @@
@mouseenter="onMouseenter" @mouseenter="onMouseenter"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
<div <button
ref="trigger" ref="trigger"
class="button-unstyled popover-trigger-button" class="button-unstyled popover-trigger-button"
type="button" type="button"
@click="onClick" @click="onClick"
> >
<slot name="trigger" /> <slot name="trigger" />
</div> </button>
<div <div
v-if="!hidden" v-if="!hidden"
ref="content" ref="content"

View file

@ -9,12 +9,11 @@ import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash' import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language'
import suggestor from '../emoji_input/suggestor.js' import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -63,13 +62,6 @@ const deleteDraft = (draftKey) => {
localStorage.setItem('drafts', JSON.stringify(draftData)); localStorage.setItem('drafts', JSON.stringify(draftData));
} }
const interfaceToISOLanguage = (ilang) => {
const sep = ilang.indexOf("_");
return sep < 0 ?
ilang :
ilang.substr(0, sep);
}
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'statusId', 'statusId',
@ -137,13 +129,6 @@ const PostStatusForm = {
this.$refs.textarea.focus() this.$refs.textarea.focus()
} }
}, },
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {
postLanguageOptions,
}
},
data () { data () {
const preset = this.$route.query.message const preset = this.$route.query.message
let statusText = preset || '' let statusText = preset || ''
@ -153,15 +138,7 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
} }
const { const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
postContentType: contentType,
postLanguage: defaultPostLanguage,
sensitiveByDefault,
sensitiveIfSubject,
interfaceLanguage,
alwaysShowSubjectInput,
} = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
let statusParams = { let statusParams = {
spoilerText: this.subject || '', spoilerText: this.subject || '',
@ -172,13 +149,12 @@ const PostStatusForm = {
poll: {}, poll: {},
mediaDescriptions: {}, mediaDescriptions: {},
visibility: this.suggestedVisibility(), visibility: this.suggestedVisibility(),
language: postLanguage, language: interfaceLanguage,
contentType contentType
} }
if (this.statusId || this.isRedraft) { if (this.statusId || this.isRedraft) {
const statusContentType = this.statusContentType || contentType const statusContentType = this.statusContentType || contentType
const statusLanguage = this.statusLanguage || language
statusParams = { statusParams = {
spoilerText: this.subject || '', spoilerText: this.subject || '',
status: this.statusText || '', status: this.statusText || '',
@ -188,7 +164,7 @@ const PostStatusForm = {
poll: this.statusPoll || {}, poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {}, mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(), visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || postLanguage, language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType contentType: statusContentType
} }
} }
@ -213,8 +189,8 @@ const PostStatusForm = {
poll: draft.data.poll, poll: draft.data.poll,
mediaDescriptions: draft.data.mediaDescriptions, mediaDescriptions: draft.data.mediaDescriptions,
visibility: draft.data.visibility, visibility: draft.data.visibility,
contentType: draft.data.contentType, language: draft.data.language,
language: draft.data.language contentType: draft.data.contentType
} }
if (draft.data.poll) { if (draft.data.poll) {
@ -223,10 +199,6 @@ const PostStatusForm = {
} }
} }
// When first loading the form, hide the subject (CW) field if it's disabled or doesn't have a starting value.
// "disableSubject" seems to take priority over "alwaysShowSubjectInput".
const showSubject = !this.disableSubject && (statusParams.spoilerText || alwaysShowSubjectInput)
return { return {
dropFiles: [], dropFiles: [],
uploadingFiles: false, uploadingFiles: false,
@ -241,10 +213,7 @@ const PostStatusForm = {
preview: null, preview: null,
previewLoading: false, previewLoading: false,
emojiInputShown: false, emojiInputShown: false,
idempotencyKey: '', idempotencyKey: ''
activeEmojiInput: undefined,
activeTextInput: undefined,
subjectVisible: showSubject
} }
}, },
computed: { computed: {
@ -333,11 +302,13 @@ const PostStatusForm = {
...mapState({ ...mapState({
mobileLayout: state => state.interface.mobileLayout mobileLayout: state => state.interface.mobileLayout
}), }),
isoLanguages () {
return iso6391.getAllCodes();
}
}, },
watch: { watch: {
'newStatus': { 'newStatus': {
deep: true, deep: true,
flush: 'sync',
handler () { handler () {
this.statusChanged() this.statusChanged()
} }
@ -703,33 +674,8 @@ const PostStatusForm = {
this.$refs['emoji-input'].resize() this.$refs['emoji-input'].resize()
}, },
showEmojiPicker () { showEmojiPicker () {
if (!this.activeEmojiInput || !this.activeTextInput) this.$refs['textarea'].focus()
this.focusStatusInput() this.$refs['emoji-input'].triggerShowPicker()
this.$refs[this.activeTextInput].focus()
this.$refs[this.activeEmojiInput].triggerShowPicker()
},
focusStatusInput() {
this.activeEmojiInput = 'emoji-input'
this.activeTextInput = 'textarea'
},
focusSubjectInput() {
this.activeEmojiInput = 'subject-emoji-input'
this.activeTextInput = 'subject-input'
},
toggleSubjectVisible() {
// If hiding CW, then we need to clear the subject and reset focus
if (this.subjectVisible)
{
this.focusStatusInput()
// "nsfw" property is normally set by the @change listener, but this bypasses it.
// We need to clear it manually instead.
this.newStatus.spoilerText = ''
this.newStatus.nsfw = false
}
this.subjectVisible = !this.subjectVisible
}, },
clearError () { clearError () {
this.error = null this.error = null

View file

@ -118,16 +118,13 @@
/> />
</div> </div>
<EmojiInput <EmojiInput
v-if="subjectVisible" v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
ref="subject-emoji-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
enable-emoji-picker enable-emoji-picker
hide-emoji-button
:suggest="emojiSuggestor" :suggest="emojiSuggestor"
class="form-control" class="form-control"
> >
<input <input
ref="subject-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
type="text" type="text"
:placeholder="$t('post_status.content_warning')" :placeholder="$t('post_status.content_warning')"
@ -135,7 +132,6 @@
size="1" size="1"
class="form-post-subject" class="form-post-subject"
@input="onSubjectInput" @input="onSubjectInput"
@focus="focusSubjectInput()"
> >
</EmojiInput> </EmojiInput>
<i18n-t <i18n-t
@ -170,18 +166,13 @@
cols="1" cols="1"
:disabled="posting && !optimisticPosting" :disabled="posting && !optimisticPosting"
class="form-post-body" class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }" :class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter=" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
$event.isComposing ||
$event.keyCode === 229 ||
(submitOnEnter && postStatus($event, newStatus))
"
@keydown.meta.enter="postStatus($event, newStatus)" @keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize" @input="resize"
@compositionupdate="resize" @compositionupdate="resize"
@paste="paste" @paste="paste"
@focus="focusStatusInput()"
/> />
<p <p
v-if="hasStatusLengthLimit" v-if="hasStatusLengthLimit"
@ -194,7 +185,6 @@
<div <div
v-if="!disableScopeSelector" v-if="!disableScopeSelector"
class="visibility-tray" class="visibility-tray"
:class="{ 'visibility-tray-edit': isEdit }"
> >
<scope-selector <scope-selector
v-if="!disableVisibilitySelector" v-if="!disableVisibilitySelector"
@ -205,50 +195,47 @@
/> />
<div <div
class="format-selector-container"> class="language-selector"
<div
class="format-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in postLanguageOptions"
:key="language.key"
:value="language.value"
>
{{ language.label }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format format-selector"
> >
<Select <Select
id="post-content-type" id="post-language"
v-model="newStatus.contentType" v-model="newStatus.language"
class="form-control" class="form-control"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</Select>
</div>
<div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
class="text-format format-selector"
> >
<span class="only-format"> <option
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }} v-for="language in isoLanguages"
</span> :key="language"
</div> :value="language"
>
{{ language }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format"
>
<Select
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</Select>
</div>
<div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
class="text-format"
>
<span class="only-format">
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -289,15 +276,6 @@
> >
<FAIcon icon="poll-h" /> <FAIcon icon="poll-h" />
</button> </button>
<button
v-if="!disableSubject"
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />
</button>
</div> </div>
<button <button
v-if="posting" v-if="posting"
@ -468,10 +446,6 @@
align-items: baseline; align-items: baseline;
} }
.visibility-tray-edit {
justify-content: right;
}
.visibility-notice.edit-warning { .visibility-notice.edit-warning {
> :first-child { > :first-child {
margin-top: 0; margin-top: 0;
@ -482,13 +456,7 @@
} }
} }
.format-selector-container { .media-upload-icon, .poll-icon, .emoji-icon {
.format-selector {
display: inline-block;
}
}
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
font-size: 1.85em; font-size: 1.85em;
line-height: 1.1; line-height: 1.1;
flex: 1; flex: 1;
@ -531,11 +499,6 @@
.poll-icon { .poll-icon {
order: 3; order: 3;
justify-content: center;
}
.spoiler-icon {
order: 4;
justify-content: right; justify-content: right;
} }
@ -588,11 +551,6 @@
line-height: 1.85; line-height: 1.85;
} }
.form-post-subject {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-post-body { .form-post-body {
// TODO: make a resizable textarea component? // TODO: make a resizable textarea component?
box-sizing: content-box; // needed for easier computation of dynamic size box-sizing: content-box; // needed for easier computation of dynamic size
@ -605,11 +563,6 @@
min-height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em);
resize: none; resize: none;
&.-has-subject {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&.scrollable-form { &.scrollable-form {
overflow-y: auto; overflow-y: auto;
} }

View file

@ -3,7 +3,7 @@
v-if="isLoggedIn && !resettingForm" v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated" :is-open="modalActivated"
class="post-form-modal-view" class="post-form-modal-view"
@backdrop-clicked="closeModal" @backdropClicked="closeModal"
> >
<div class="post-form-modal-panel panel"> <div class="post-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">

View file

@ -8,13 +8,13 @@
remove-padding remove-padding
@show="focusInput" @show="focusInput"
> >
<template #content="{close}"> <template v-slot:content="{close}">
<EmojiPicker <EmojiPicker
:enable-sticker-picker="false" :enable-sticker-picker="false"
@emoji="addReaction($event, close)" @emoji="addReaction($event, close)"
/> />
</template> </template>
<template #trigger> <template v-slot:trigger>
<button <button
class="button-unstyled popover-trigger" class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"
@ -28,7 +28,7 @@
</Popover> </Popover>
</template> </template>
<script src="./react_button.js"></script> <script src="./react_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -2,7 +2,7 @@ export default {
props: [ 'user' ], props: [ 'user' ],
computed: { computed: {
subscribeUrl () { subscribeUrl () {
// eslint-disable-next-line no-undef
const serverUrl = new URL(this.user.statusnet_profile_url) const serverUrl = new URL(this.user.statusnet_profile_url)
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
} }

View file

@ -54,7 +54,7 @@
</div> </div>
</template> </template>
<script src="./retweet_button.js"></script> <script src="./retweet_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -1,54 +1,38 @@
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import Status from '../status/status.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import SearchFilters from './search_filters.vue'
import map from 'lodash/map' import map from 'lodash/map'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faCircleNotch, faCircleNotch,
faSearch faSearch
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { uniqBy } from 'lodash'
library.add( library.add(
faCircleNotch, faCircleNotch,
faSearch faSearch
) )
const allSearchTypes = Object.freeze(['statuses', 'media', 'accounts', 'hashtags'])
const Search = { const Search = {
components: { components: {
FollowCard, FollowCard,
Conversation, Conversation,
TabSwitcher, Status,
SearchFilters TabSwitcher
}, },
props: [ props: [
'query' 'query'
], ],
data () { data () {
return { return {
queryCount: 0, loaded: false,
loadedInitially: false, loading: false,
loading: Object.fromEntries(
allSearchTypes.map((searchType) => [searchType, false])
),
searchTerm: this.query || '', searchTerm: this.query || '',
userIds: [], userIds: [],
statuses: [], statuses: [],
media: [],
hashtags: [], hashtags: [],
allTabs: allSearchTypes, currenResultTab: 'statuses'
currentResultTab: 'statuses',
hasUserSelectedTab: false,
statusesOffset: 0,
lastStatusFetchCount: 0,
mediaOffset: 0,
lastMediaFetchCount: 0,
lastQuery: '',
filter: {}
} }
}, },
computed: { computed: {
@ -61,20 +45,6 @@ const Search = {
return this.statuses.filter(status => return this.statuses.filter(status =>
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
) )
},
visibleMedia () {
const allStatusesObject = this.$store.state.statuses.allStatusesObject
return this.media.filter(status =>
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
)
},
canSearchMediaPosts () {
return this.$store.state.instance.searchTypeMediaEnabled === true
},
hasAtLeastOneResult () {
return allSearchTypes
.some((searchType) => this.getVisibleLength(searchType) > 0)
} }
}, },
mounted () { mounted () {
@ -84,191 +54,55 @@ const Search = {
query (newValue) { query (newValue) {
this.searchTerm = newValue this.searchTerm = newValue
this.search(newValue) this.search(newValue)
},
filter: {
deep: true,
handler () {
this.lastQuery = "" // invalidate state
this.search(this.searchTerm)
}
} }
}, },
methods: { methods: {
newQuery (query) { newQuery (query) {
if (this.lastQuery === query && !this.loading[this.currentResultTab]) { this.$router.push({ name: 'search', query: { query } })
// Handle search retries this.$refs.searchInput.focus()
this.lastQuery = "" // invalidate state
this.search(query)
} else {
this.$router.push({ name: 'search', query: { query } })
this.$refs.searchInput.focus()
}
}, },
async search (query, searchType = null) { search (query) {
if (!query) { if (!query) {
for (const searchType of allSearchTypes) { this.loading = false
this.loading[searchType] = false
}
return return
} }
const localQueryCount = ++this.queryCount this.loading = true
this.userIds = []
const isNewSearch = this.lastQuery !== query this.statuses = []
this.hashtags = []
this.$refs.searchInput.blur() this.$refs.searchInput.blur()
if (isNewSearch) {
this.userIds = []
this.hashtags = []
this.statuses = []
this.media = []
this.statusesOffset = 0 this.$store.dispatch('search', { q: query, resolve: true })
this.lastStatusFetchCount = 0 .then(data => {
this.mediaOffset = 0 this.loading = false
this.lastMediaFetchCount = 0 this.userIds = map(data.accounts, 'id')
} this.statuses = data.statuses
this.hashtags = data.hashtags
let searchTypes = allSearchTypes this.currenResultTab = this.getActiveTab()
if (searchType) { this.loaded = true
// Search only for `searchType` if it is provided })
searchTypes = [searchType]
} else if (this.hasUserSelectedTab && this.currentResultTab !== 'statuses') {
// Start the search from the tab that the user has selected
// No need to sort if userPreferredTab === 'statuses'
searchTypes = [
this.currentResultTab,
...allSearchTypes.filter((tab) => tab !== this.currentResultTab)
]
}
for (const searchType of searchTypes) {
this.loading[searchType] = true
}
let oldStatusesLength = this.statuses.length
let oldMediaLength = this.media.length
let skipMediaSearch = !this.canSearchMediaPosts
for (const searchType of searchTypes) {
try {
if (searchType === 'media' && skipMediaSearch) {
continue
}
let searchOffset
if (searchType === 'statuses') {
searchOffset = this.statusesOffset
} else if (searchType === 'media') {
searchOffset = this.mediaOffset
}
const data = await this.$store.dispatch('search', {
q: query,
resolve: true,
offset: searchOffset,
'type': searchType,
following:
'followingOnly' in this.filter && this.filter.followingOnly,
local: 'localOnly' in this.filter && this.filter.localOnly
})
if (localQueryCount !== this.queryCount) {
// Query count differs, there should be a newer query
return
}
// Always append to old results. If new results are empty, this doesn't change anything
this.userIds = this.userIds.concat(map(data.accounts, 'id'))
this.statuses = uniqBy(this.statuses.concat(data.statuses), 'id')
if ('media' in data) {
this.media = uniqBy(this.media.concat(data.media), 'id')
}
this.hashtags = this.hashtags.concat(data.hashtags)
if (isNewSearch) {
if (!this.hasUserSelectedTab) {
this.currentResultTab = this.getFirstTabWithResults()
}
if (searchType === 'statuses' && data.statuses.length === 0) {
// Safe to assume that there are no media posts
skipMediaSearch = true
}
}
} catch (error) {
console.error(error)
} finally {
if (localQueryCount !== this.queryCount) {
// Skip cleanups if there's a newer query
return
}
if (!this.loadedInitially && this.hasAtLeastOneResult) {
// Show results on the first meaningful response
this.loadedInitially = true
}
this.loading[searchType] = false
if (searchType === 'statuses') {
// Offset from whatever we already have
this.statusesOffset = this.statuses.length
// Because the amount of new statuses can actually be zero, compare to old length instead
this.lastStatusFetchCount = this.statuses.length - oldStatusesLength
} else if (searchType === 'media') {
this.mediaOffset = this.media.length
this.lastMediaFetchCount = this.media.length - oldMediaLength
}
}
}
this.lastQuery = query
this.loadedInitially = true
for (const searchType of allSearchTypes) {
this.loading[searchType] = false
}
}, },
resultCount (tab) { resultCount (tabName) {
const length = this.getVisibleLength(tab) const length = this[tabName].length
return length === 0 ? '' : ` (${length})`
if (length === 0 || !this.loadedInitially) {
return ''
}
if (
(tab === 'statuses' && this.lastStatusFetchCount !== 0) ||
(tab === 'media' && this.lastMediaFetchCount !== 0)
) {
return ` (${length}+)`
}
return ` (${length})`
}, },
onResultTabSwitch (key) { onResultTabSwitch (key) {
this.currentResultTab = key this.currenResultTab = key
this.hasUserSelectedTab = true
}, },
getFirstTabWithResults () { getActiveTab () {
for (const tab of allSearchTypes) { if (this.visibleStatuses.length > 0) {
if (this.getVisibleLength(tab) > 0) { return 'statuses'
return tab } else if (this.users.length > 0) {
} return 'people'
} else if (this.hashtags.length > 0) {
return 'hashtags'
} }
return 'statuses' return 'statuses'
}, },
lastHistoryRecord (hashtag) { lastHistoryRecord (hashtag) {
return hashtag.history && hashtag.history[0] return hashtag.history && hashtag.history[0]
},
getVisibleLength (tab) {
if (tab === 'statuses') {
return this.visibleStatuses.length
} else if (tab === 'media') {
return this.visibleMedia.length
} else if (tab === 'accounts') {
return this.users.length
} else if (tab === 'hashtags') {
return this.hashtags.length
}
} }
} }
} }

View file

@ -4,9 +4,6 @@
<div class="title"> <div class="title">
{{ $t('nav.search') }} {{ $t('nav.search') }}
</div> </div>
<SearchFilters
v-model="filter"
/>
</div> </div>
<div class="search-input-container"> <div class="search-input-container">
<input <input
@ -14,9 +11,7 @@
v-model="searchTerm" v-model="searchTerm"
class="search-input" class="search-input"
:placeholder="$t('nav.search')" :placeholder="$t('nav.search')"
@keydown.enter=" @keyup.enter="newQuery(searchTerm)"
$event.isComposing || $event.keyCode === 229 || newQuery(searchTerm)
"
> >
<button <button
class="btn button-default search-button" class="btn button-default search-button"
@ -26,25 +21,30 @@
<FAIcon icon="search" /> <FAIcon icon="search" />
</button> </button>
</div> </div>
<div v-if="loadedInitially"> <div
v-if="loading"
class="text-center loading-icon"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
<div v-else-if="loaded">
<div class="search-nav-heading"> <div class="search-nav-heading">
<tab-switcher <tab-switcher
ref="tabSwitcher" ref="tabSwitcher"
:on-switch="onResultTabSwitch" :on-switch="onResultTabSwitch"
:active-tab="currentResultTab" :active-tab="currenResultTab"
> >
<span <span
key="statuses" key="statuses"
:label="$t('user_card.statuses') + resultCount('statuses')" :label="$t('user_card.statuses') + resultCount('visibleStatuses')"
/> />
<span <span
v-if="canSearchMediaPosts" key="people"
key="media" :label="$t('search.people') + resultCount('users')"
:label="$t('user_card.media') + resultCount('media')"
/>
<span
key="accounts"
:label="$t('search.people') + resultCount('accounts')"
/> />
<span <span
key="hashtags" key="hashtags"
@ -54,97 +54,27 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div <div v-if="currenResultTab === 'statuses'">
v-if="!Object.values(loading).includes(false) && !hasAtLeastOneResult" <div
class="text-center loading-icon" v-if="visibleStatuses.length === 0 && !loading && loaded"
> class="search-result-heading"
<FAIcon >
icon="circle-notch" <h4>{{ $t('search.no_results') }}</h4>
spin </div>
size="lg" <Status
/>
</div>
<div v-if="currentResultTab === 'statuses'">
<Conversation
v-for="status in visibleStatuses" v-for="status in visibleStatuses"
:key="status.id" :key="status.id"
:collapsable="true" :collapsable="false"
class="status-fadein" :expandable="false"
:status-id="status.id" :compact="false"
class="search-result"
:statusoid="status"
:no-heading="false"
/> />
<button
v-if="!loading['statuses'] && loadedInitially && lastStatusFetchCount > 0"
class="more-statuses-button button-unstyled -link -fullwidth"
@click.prevent="search(lastQuery, 'statuses')"
>
<div class="new-status-notification text-center">
{{ $t('search.load_more') }}
</div>
</button>
<div
v-else-if="loading['statuses'] && statusesOffset > 0"
class="text-center loading-icon"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
<div
v-if="
(visibleStatuses.length === 0 || lastStatusFetchCount === 0) &&
!loading['statuses'] && loadedInitially
"
class="search-result-heading"
>
<h4>
{{ visibleStatuses.length === 0 ? $t('search.no_results') : $t('search.no_more_results') }}
</h4>
</div>
</div> </div>
<div v-if="currentResultTab === 'media'"> <div v-else-if="currenResultTab === 'people'">
<Conversation
v-for="media in visibleMedia"
:key="media.id"
:collapsable="true"
class="status-fadein"
:status-id="media.id"
/>
<button
v-if="!loading['media'] && loadedInitially && lastMediaFetchCount > 0"
class="more-statuses-button button-unstyled -link -fullwidth"
@click.prevent="search(lastQuery, 'media')"
>
<div class="new-status-notification text-center">
{{ $t('search.load_more') }}
</div>
</button>
<div <div
v-else-if="loading['media'] && mediaOffset > 0" v-if="users.length === 0 && !loading && loaded"
class="text-center loading-icon"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
<div
v-if="
(visibleMedia.length === 0 || lastMediaFetchCount === 0) &&
!loading['media'] && loadedInitially
"
class="search-result-heading"
>
<h4>
{{ visibleMedia.length === 0 ? $t('search.no_results') : $t('search.no_more_results') }}
</h4>
</div>
</div>
<div v-else-if="currentResultTab === 'accounts'">
<div
v-if="users.length === 0 && !loading['accounts'] && loadedInitially"
class="search-result-heading" class="search-result-heading"
> >
<h4>{{ $t('search.no_results') }}</h4> <h4>{{ $t('search.no_results') }}</h4>
@ -156,9 +86,9 @@
class="list-item search-result" class="list-item search-result"
/> />
</div> </div>
<div v-else-if="currentResultTab === 'hashtags'"> <div v-else-if="currenResultTab === 'hashtags'">
<div <div
v-if="hashtags.length === 0 && !loading['hashtags'] && loadedInitially" v-if="hashtags.length === 0 && !loading && loaded"
class="search-result-heading" class="search-result-heading"
> >
<h4>{{ $t('search.no_results') }}</h4> <h4>{{ $t('search.no_results') }}</h4>
@ -217,6 +147,13 @@
} }
} }
.search-result {
box-sizing: border-box;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.search-result-footer { .search-result-footer {
border-width: 1px 0 0 0; border-width: 1px 0 0 0;
border-style: solid; border-style: solid;
@ -241,10 +178,6 @@
.search-button { .search-button {
margin-left: 0.5em; margin-left: 0.5em;
width: 3em;
display: flex;
justify-content: center;
align-items: center;
} }
} }
@ -256,12 +189,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
& + & {
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.hashtag { .hashtag {
flex: 1 1 auto; flex: 1 1 auto;
color: $fallback--text; color: $fallback--text;
@ -269,12 +196,6 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
box-sizing: border-box;
a {
display: inline-block;
padding: .5em 1em;
}
} }
.count { .count {
@ -289,8 +210,4 @@
} }
} }
.more-statuses-button {
height: 3.5em;
line-height: 3.5em;
}
</style> </style>

View file

@ -1,116 +0,0 @@
<template>
<Popover
v-if="showFilters"
trigger="click"
class="SearchFilters"
placement="bottom"
:bound-to="{ x: 'container' }"
>
<template #content>
<div class="dropdown-menu">
<button
v-if="canSearchFollowing"
class="button-default dropdown-item"
@click="toggleFilter('followingOnly')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': currentFilter.followingOnly }"
/>{{ $t('lists.following_only') }}
</button>
<button
v-if="canSearchLocal"
class="button-default dropdown-item"
@click="toggleFilter('localOnly')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': currentFilter.localOnly }"
/>{{ $t('search.local_only') }}
</button>
</div>
</template>
<template #trigger>
<button class="filter-trigger-button button-unstyled">
<FAIcon icon="filter" />
</button>
</template>
</Popover>
</template>
<script>
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
library.add(
faFilter
)
export default {
components: { Popover },
props: [
'modelValue'
],
emits: [
'update:modelValue'
],
data () {
return {
currentFilter: {
followingOnly: false,
localOnly: false
}
}
},
computed: {
showFilters () {
return this.canSearchFollowing || this.canSearchLocal
},
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
canSearchFollowing () {
return this.isLoggedIn &&
this.$store.state.instance.searchOptionFollowingEnabled
},
canSearchLocal () {
return this.$store.state.instance.searchOptionLocalEnabled
},
},
created () {
for (const filterName of Object.entries(this.currentFilter)) {
if (this.modelValue && filterName in this.modelValue) {
this.currentFilter[filterName] = this.modelValue[filterName]
}
}
},
methods: {
toggleFilter (filterName) {
if (filterName in this.currentFilter) {
this.currentFilter[filterName] = !this.currentFilter[filterName]
} else {
this.currentFilter[filterName] = true
}
this.$emit('update:modelValue', this.currentFilter)
}
}
}
</script>
<style lang="scss">
.SearchFilters {
align-self: stretch;
> button {
line-height: 100%;
height: 100%;
width: var(--__panel-heading-height-inner);
text-align: center;
svg {
font-size: 1.2em;
}
}
}
</style>

View file

@ -10,12 +10,6 @@ library.add(
) )
const SearchBar = { const SearchBar = {
mounted () {
window.addEventListener('keydown', this.autoFocus)
},
beforeDestroy () {
window.removeEventListener('keydown', this.autoFocus)
},
data: () => ({ data: () => ({
searchTerm: undefined, searchTerm: undefined,
hidden: true, hidden: true,
@ -42,38 +36,6 @@ const SearchBar = {
this.$refs.searchInput.focus() this.$refs.searchInput.focus()
} }
}) })
},
autoFocus (event) {
if (
event.target.tagName !== 'BODY' ||
event.altKey ||
event.ctrlKey ||
event.isComposing ||
event.metaKey ||
event.repeat ||
event.shiftKey ||
// not very vue-esque, but as long as it works
document.querySelector('.modal-view.modal-background.open')
) {
return
}
if (event.key === '/') {
if (this.hidden) {
this.toggleHidden()
} else {
this.$refs.searchInput.focus()
}
event.preventDefault()
} else if (event.key === 'Escape') {
if (!this.hidden) {
this.toggleHidden()
}
event.preventDefault()
}
},
blur () {
this.$refs.searchInput.blur()
} }
} }
} }

View file

@ -24,12 +24,7 @@
class="search-bar-input" class="search-bar-input"
:placeholder="$t('nav.search')" :placeholder="$t('nav.search')"
type="text" type="text"
@keydown.enter=" @keyup.enter="find(searchTerm)"
$event.isComposing || $event.keyCode === 229 || find(searchTerm)
"
@keydown.escape="
$event.isComposing || blur()
"
> >
<button <button
class="button-default search-button" class="button-default search-button"
@ -63,7 +58,8 @@
.SearchBar { .SearchBar {
display: inline-flex; display: inline-flex;
align-items: center; align-items: baseline;
vertical-align: baseline;
justify-content: flex-end; justify-content: flex-end;
&.-expanded { &.-expanded {
@ -75,21 +71,13 @@
height: 29px; height: 29px;
} }
.search-button {
margin-left: 0.5em;
width: 3em;
display: flex;
justify-content: center;
align-items: center;
}
.search-bar-input { .search-bar-input {
flex: 1 0 auto; flex: 1 0 auto;
margin-left: 0.5em; margin-left: 0.5em;
} }
.cancel-search { .cancel-search {
height: 100%; height: 50px;
} }
.cancel-icon { .cancel-icon {

View file

@ -24,7 +24,7 @@
:items="items" :items="items"
:get-key="getKey" :get-key="getKey"
> >
<template #item="{item}"> <template v-slot:item="{item}">
<div <div
class="selectable-list-item-inner" class="selectable-list-item-inner"
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
@ -41,7 +41,7 @@
/> />
</div> </div>
</template> </template>
<template #empty> <template v-slot:empty>
<slot name="empty" /> <slot name="empty" />
</template> </template>
</List> </List>

View file

@ -6,7 +6,7 @@
<Checkbox <Checkbox
:model-value="state" :model-value="state"
:disabled="disabled" :disabled="disabled"
@update:model-value="update" @update:modelValue="update"
> >
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"

View file

@ -8,7 +8,7 @@
<Select <Select
:model-value="state" :model-value="state"
:disabled="disabled" :disabled="disabled"
@update:model-value="update" @update:modelValue="update"
> >
<option <option
v-for="option in options" v-for="option in options"

View file

@ -6,14 +6,14 @@
<Popover <Popover
trigger="hover" trigger="hover"
> >
<template #trigger> <template v-slot:trigger>
&nbsp; &nbsp;
<FAIcon <FAIcon
icon="wrench" icon="wrench"
:aria-label="$t('settings.setting_changed')" :aria-label="$t('settings.setting_changed')"
/> />
</template> </template>
<template #content> <template v-slot:content>
<div class="modified-tooltip"> <div class="modified-tooltip">
{{ $t('settings.setting_changed') }} {{ $t('settings.setting_changed') }}
</div> </div>

View file

@ -6,14 +6,14 @@
<Popover <Popover
trigger="hover" trigger="hover"
> >
<template #trigger> <template v-slot:trigger>
&nbsp; &nbsp;
<FAIcon <FAIcon
icon="server" icon="server"
:aria-label="$t('settings.setting_server_side')" :aria-label="$t('settings.setting_server_side')"
/> />
</template> </template>
<template #content> <template v-slot:content>
<div class="serverside-tooltip"> <div class="serverside-tooltip">
{{ $t('settings.setting_server_side') }} {{ $t('settings.setting_server_side') }}
</div> </div>

View file

@ -69,7 +69,7 @@ const SettingsModal = {
this.$store.dispatch('closeSettingsModal') this.$store.dispatch('closeSettingsModal')
}, },
logout () { logout () {
this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all') this.$router.replace('/main/public')
this.$store.dispatch('closeSettingsModal') this.$store.dispatch('closeSettingsModal')
this.$store.dispatch('logout') this.$store.dispatch('logout')
}, },

View file

@ -4,7 +4,6 @@
class="settings-modal" class="settings-modal"
:class="{ peek: modalPeeked }" :class="{ peek: modalPeeked }"
:no-background="modalPeeked" :no-background="modalPeeked"
@backdropClicked="closeModal"
> >
<div class="settings-modal-panel panel"> <div class="settings-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
@ -109,7 +108,7 @@
<Checkbox <Checkbox
:model-value="!!expertLevel" :model-value="!!expertLevel"
class="expertMode" class="expertMode"
@update:model-value="expertLevel = Number($event)" @update:modelValue="expertLevel = Number($event)"
> >
{{ $t("settings.expert_mode") }} {{ $t("settings.expert_mode") }}
</Checkbox> </Checkbox>

View file

@ -72,7 +72,7 @@ const DataImportExportTab = {
// check is it's a local user // check is it's a local user
if (user && user.is_local) { if (user && user.is_local) {
// append the instance address // append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname return user.screen_name + '@' + location.hostname
} }
return user.screen_name return user.screen_name

View file

@ -4,14 +4,12 @@ import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import { usePostLanguageOptions } from 'src/lib/post_language'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue' import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faGlobe, faSync faGlobe, faSync
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import iso6391 from 'iso-639-1'
library.add( library.add(
faGlobe, faGlobe,
@ -19,11 +17,6 @@ library.add(
) )
const GeneralTab = { const GeneralTab = {
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {postLanguageOptions}
},
data () { data () {
return { return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({ subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
@ -87,15 +80,6 @@ const GeneralTab = {
label: this.$t(`post_status.content_type["${format}"]`) label: this.$t(`post_status.content_type["${format}"]`)
})) }))
}, },
postLanguages () {
return iso6391.getLanguages(iso6391.getAllCodes())
.map(lang => ({
key: lang.code,
value: lang.code,
label: lang.nativeName,
}))
.sort((a, b) => a.label.localeCompare(b.label));
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
instanceWallpaperUsed () { instanceWallpaperUsed () {
return this.$store.state.instance.background && return this.$store.state.instance.background &&
@ -134,12 +118,6 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val }) this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
} }
}, },
postLanguage: {
get: function () { return this.$store.getters.mergedConfig.postLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
}
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {

View file

@ -44,6 +44,7 @@
<template <template
v-if="profilesExpanded" v-if="profilesExpanded"
> >
<div <div
v-for="profile in settingsProfiles" v-for="profile in settingsProfiles"
:key="profile.id" :key="profile.id"
@ -72,24 +73,15 @@
</button> </button>
</template> </template>
</div> </div>
<button <button class="btn button-default" @click="refreshProfiles()">
class="btn button-default"
@click="refreshProfiles()"
>
{{ $t('settings.settings_profiles_refresh') }} {{ $t('settings.settings_profiles_refresh') }}
<FAIcon <FAIcon icon="sync" @click="refreshProfiles()" />
icon="sync"
@click="refreshProfiles()"
/>
</button> </button>
<h3>{{ $t('settings.settings_profile_creation') }}</h3> <h3>{{ $t('settings.settings_profile_creation') }}</h3>
<label for="settings-profile-new-name"> <label for="settings-profile-new-name">
{{ $t('settings.settings_profile_creation_new_name_label') }} {{ $t('settings.settings_profile_creation_new_name_label') }}
</label> </label>
<input <input v-model="newProfileName" id="settings-profile-new-name">
id="settings-profile-new-name"
v-model="newProfileName"
>
<button <button
class="btn button-default" class="btn button-default"
@click="createSettingsProfile" @click="createSettingsProfile"
@ -154,21 +146,6 @@
{{ $t('settings.show_wider_shortcuts') }} {{ $t('settings.show_wider_shortcuts') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="displayPageBackgrounds">
{{ $t('settings.show_page_backgrounds') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="centerAlignBio">
{{ $t('settings.center_align_bio') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="compactUserInfo">
{{ $t('settings.compact_user_info') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="stopGifs"> <BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }} {{ $t('settings.stop_gifs') }}
@ -506,6 +483,14 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
</ul> </ul>
<li>
<BooleanSetting
path="useAtIcon"
expert="1"
>
{{ $t('settings.use_at_icon') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="mentionLinkShowAvatar"> <BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }} {{ $t('settings.mention_link_show_avatar') }}
@ -594,15 +579,6 @@
{{ $t('settings.subject_line_behavior') }} {{ $t('settings.subject_line_behavior') }}
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<li>
<ChoiceSetting
id="postLanguage"
path="postLanguage"
:options="postLanguages"
>
{{ $t('settings.post_status_language') }}
</ChoiceSetting>
</li>
<li v-if="postFormats.length > 0"> <li v-if="postFormats.length > 0">
<ChoiceSetting <ChoiceSetting
id="postContentType" id="postContentType"
@ -612,15 +588,6 @@
{{ $t('settings.post_status_content_type') }} {{ $t('settings.post_status_content_type') }}
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<li>
<ChoiceSetting
id="postLanguage"
path="postLanguage"
:options="postLanguageOptions"
>
{{ $t('settings.post_language') }}
</ChoiceSetting>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
path="alwaysShowNewPostButton" path="alwaysShowNewPostButton"

View file

@ -85,7 +85,7 @@ const MutesAndBlocks = {
// check is it's a local user // check is it's a local user
if (user && user.is_local) { if (user && user.is_local) {
// append the instance address // append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname return user.screen_name + '@' + location.hostname
} }
return user.screen_name return user.screen_name

View file

@ -10,7 +10,7 @@
:query="queryUserIds" :query="queryUserIds"
:placeholder="$t('settings.search_user_to_block')" :placeholder="$t('settings.search_user_to_block')"
> >
<template #default="row"> <template v-slot="row">
<BlockCard <BlockCard
:user-id="row.item" :user-id="row.item"
/> />
@ -21,7 +21,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template #header="{selected}"> <template v-slot:header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -29,7 +29,7 @@
:click="() => blockUsers(selected)" :click="() => blockUsers(selected)"
> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
<template #progress> <template v-slot:progress>
{{ $t('user_card.block_progress') }} {{ $t('user_card.block_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -39,16 +39,16 @@
:click="() => unblockUsers(selected)" :click="() => unblockUsers(selected)"
> >
{{ $t('user_card.unblock') }} {{ $t('user_card.unblock') }}
<template #progress> <template v-slot:progress>
{{ $t('user_card.unblock_progress') }} {{ $t('user_card.unblock_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> </template>
<template #item="{item}"> <template v-slot:item="{item}">
<BlockCard :user-id="item" /> <BlockCard :user-id="item" />
</template> </template>
<template #empty> <template v-slot:empty>
{{ $t('settings.no_blocks') }} {{ $t('settings.no_blocks') }}
</template> </template>
</BlockList> </BlockList>
@ -63,7 +63,7 @@
:query="queryUserIds" :query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')" :placeholder="$t('settings.search_user_to_mute')"
> >
<template #default="row"> <template v-slot="row">
<MuteCard <MuteCard
:user-id="row.item" :user-id="row.item"
/> />
@ -74,7 +74,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template #header="{selected}"> <template v-slot:header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -82,7 +82,7 @@
:click="() => muteUsers(selected)" :click="() => muteUsers(selected)"
> >
{{ $t('user_card.mute') }} {{ $t('user_card.mute') }}
<template #progress> <template v-slot:progress>
{{ $t('user_card.mute_progress') }} {{ $t('user_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -92,16 +92,16 @@
:click="() => unmuteUsers(selected)" :click="() => unmuteUsers(selected)"
> >
{{ $t('user_card.unmute') }} {{ $t('user_card.unmute') }}
<template #progress> <template v-slot:progress>
{{ $t('user_card.unmute_progress') }} {{ $t('user_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> </template>
<template #item="{item}"> <template v-slot:item="{item}">
<MuteCard :user-id="item" /> <MuteCard :user-id="item" />
</template> </template>
<template #empty> <template v-slot:empty>
{{ $t('settings.no_mutes') }} {{ $t('settings.no_mutes') }}
</template> </template>
</MuteList> </MuteList>
@ -114,7 +114,7 @@
:query="queryKnownDomains" :query="queryKnownDomains"
:placeholder="$t('settings.type_domains_to_mute')" :placeholder="$t('settings.type_domains_to_mute')"
> >
<template #default="row"> <template v-slot="row">
<DomainMuteCard <DomainMuteCard
:domain="row.item" :domain="row.item"
/> />
@ -125,7 +125,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template #header="{selected}"> <template v-slot:header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -133,16 +133,16 @@
:click="() => unmuteDomains(selected)" :click="() => unmuteDomains(selected)"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template #progress> <template v-slot:progress>
{{ $t('domain_mute_card.unmute_progress') }} {{ $t('domain_mute_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> </template>
<template #item="{item}"> <template v-slot:item="{item}">
<DomainMuteCard :domain="item" /> <DomainMuteCard :domain="item" />
</template> </template>
<template #empty> <template v-slot:empty>
{{ $t('settings.no_mutes') }} {{ $t('settings.no_mutes') }}
</template> </template>
</DomainMuteList> </DomainMuteList>

View file

@ -33,7 +33,6 @@ const ProfileTab = {
newName: this.$store.state.users.currentUser.name_unescaped, newName: this.$store.state.users.currentUser.name_unescaped,
newBio: unescape(this.$store.state.users.currentUser.description), newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked, newLocked: this.$store.state.users.currentUser.locked,
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
showRole: this.$store.state.users.currentUser.show_role, showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role, role: this.$store.state.users.currentUser.role,
@ -130,15 +129,14 @@ const ProfileTab = {
note: this.newBio, note: this.newBio,
locked: this.newLocked, locked: this.newLocked,
// Backend notation. // Backend notation.
/* eslint-disable camelcase */
display_name: this.newName, display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null), fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot, bot: this.bot,
show_role: this.showRole, show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1, status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
permit_followback: this.permit_followback,
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
/* eslint-enable camelcase */
} }
if (this.emailLanguage) { if (this.emailLanguage) {
@ -187,7 +185,7 @@ const ProfileTab = {
}) })
return return
} }
// eslint-disable-next-line no-undef
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({ target }) => { reader.onload = ({ target }) => {
const img = target.result const img = target.result

View file

@ -110,9 +110,11 @@
max="730" max="730"
class="expire-posts-days" class="expire-posts-days"
:placeholder="$t('settings.expire_posts_input_placeholder')" :placeholder="$t('settings.expire_posts_input_placeholder')"
> />
</p>
<p>
</p> </p>
<p />
<p> <p>
<interface-language-switcher <interface-language-switcher
:prompt-text="$t('settings.email_language')" :prompt-text="$t('settings.email_language')"
@ -257,19 +259,6 @@
<BooleanSetting path="serverSide_locked"> <BooleanSetting path="serverSide_locked">
{{ $t('settings.lock_account_description') }} {{ $t('settings.lock_account_description') }}
</BooleanSetting> </BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !serverSide_locked}]"
>
<li>
<BooleanSetting
path="serverSide_permitFollowback"
:disabled="!serverSide_locked"
>
{{ $t('settings.permit_followback_description') }}
</BooleanSetting>
</li>
</ul>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_discoverable"> <BooleanSetting path="serverSide_discoverable">

View file

@ -21,10 +21,8 @@
</div> </div>
<div class="panel-body theme-preview-content"> <div class="panel-body theme-preview-content">
<div class="post"> <div class="post">
<div class="Avatar post-avatar"> <div class="avatar still-image">
<div class="still-image avatar -better-shadow"> ( ͡° ͜ʖ ͡°)
( ͡° ͜ʖ ͡°)
</div>
</div> </div>
<div class="content"> <div class="content">
<h4> <h4>

View file

@ -1,25 +1,22 @@
import { extractCommit } from 'src/services/version/version.service' import { extractCommit } from 'src/services/version/version.service'
function joinURL(base, subpath) { const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
return URL.parse(subpath, base)?.href || "invalid base URL" const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
}
const VersionTab = { const VersionTab = {
data () { data () {
const instance = this.$store.state.instance const instance = this.$store.state.instance
return { return {
backendCommitUrl: instance.backendCommitUrl,
backendVersion: instance.backendVersion, backendVersion: instance.backendVersion,
frontendCommitUrl: instance.frontendCommitUrl,
frontendVersion: instance.frontendVersion frontendVersion: instance.frontendVersion
} }
}, },
computed: { computed: {
frontendVersionLink () { frontendVersionLink () {
return joinURL(this.frontendCommitUrl, this.frontendVersion) return pleromaFeCommitUrl + this.frontendVersion
}, },
backendVersionLink () { backendVersionLink () {
return joinURL(this.backendCommitUrl, extractCommit(this.backendVersion)) return pleromaBeCommitUrl + extractCommit(this.backendVersion)
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show more