Compare commits

..

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

243 changed files with 8121 additions and 12934 deletions

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
}
}

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/arm64
steps:
platform: linux/amd64
pipeline:
lint:
when:
event:
- pull_request
image: node:20
image: node:18
commands:
- yarn
- yarn lint
#- yarn stylelint
test:
when:
event:
- pull_request
image: node:20
image: node:18
commands:
- apt update
- apt install firefox-esr -y --no-install-recommends
- yarn
- yarn
- yarn unit
build:
@ -29,7 +28,7 @@ steps:
branch:
- develop
- stable
image: node:20
image: node:18
commands:
- yarn
- yarn build
@ -41,18 +40,15 @@ steps:
branch:
- develop
- stable
image: node:20
environment:
SCW_ACCESS_KEY:
from_secret: SCW_ACCESS_KEY
SCW_SECRET_KEY:
from_secret: SCW_SECRET_KEY
SCW_DEFAULT_ORGANIZATION_ID:
from_secret: SCW_DEFAULT_ORGANIZATION_ID
image: node:18
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_arm64
- mv scaleway-cli_2.30.0_linux_arm64 scaleway-cli
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- zip akkoma-fe.zip -r dist
@ -67,17 +63,15 @@ steps:
- stable
environment:
CI: "true"
SCW_ACCESS_KEY:
from_secret: SCW_ACCESS_KEY
SCW_SECRET_KEY:
from_secret: SCW_SECRET_KEY
SCW_DEFAULT_ORGANIZATION_ID:
from_secret: SCW_DEFAULT_ORGANIZATION_ID
image: python:3.10-slim
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_arm64
- mv scaleway-cli_2.30.0_linux_arm64 scaleway-cli
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- cd docs
@ -85,4 +79,4 @@ steps:
- mkdocs build
- zip -r docs.zip site/*
- cd site
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/

View file

@ -5,70 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added
- the search interface now exposes more control knobs:
- post search can now be limited to a single post author
- user search can now be limited to followed users
- resolving remote URLs or WebFinger lookups are now optional
- search can be limited to a particular type for faster responses
- search now allows fetching additional matches past the first page
- further extended MFM support
(ruby, unixtime, fade, border, crop, animation loop count, animation delay and some fixes)
- it is now possible to view poll results without voting
### Fixed
- favourite and repeat indicators no longer occasionally disappear after a successfull interaction
- favourite and repeat indicators no longer wrongly pretend success after a failed interaction
### Changed
- various minor visual styling enhancements
- overly long polls can and are now collapsed analogous to overly long status texts
## 2026.05 (3.19) - 2026-05-04
### Added
- lists UI can now read and set the "exclusive" parameter, allowing members to be removed from the home timeline
- user profiles now have a small gallery for profile media
- alt text of user profile media is now exposed and can be edited in profile settings
- if known, polls now show what promise wrt to vote anonymity was made
### Fixed
- fix error on list creation preventing initial accounts from being actually added
- fix notifications on mobile
- fix attachment display for remotes not federating any MIME type indicators if they still indicate a generic type.
This applies to e.g. bridgy
- fixed some spacing issues after mentions
- MFM statuses now use the same emoji base size as *keys for better compatability
### Changed
- reworked rich content (anything with custom emoji or not pure plaintext) parsing;
there _should_ be no visible changes except fixing obviously broken edgecases
and perhaps more reliable green- and cyantext styling
- the frontend now also applies its own HTML sanitisation instead of relying solely on the backends sanitisation;
this shouldn't cause any visible changes but further hardens against potential future bugs
- various minor visual styling enhancements
## 2026.03 (3.18.0) - 2026-03-14
### REMOVED
- dropped obsolete and buggy dm timeline
### Added
- UI for conversations API, replacing the DM timeline.
Here each thread (conversation) has its own timeline and read markers instead of mixing everything together.
- Boosts now show when and with which visibility they were boosted
- bookmarks are now accessible via the narrow/mobile UI
### Fixed
- fixed saving fallback cop yof settings to local browser storage
- improve image animation detection further
- fix status content parsing for mention and hashtag detection; this could lock the UI until reload
- fix display of nsfw attachment overlays on webkit
## Between 2022.09 (3.2.0) and 2025.12 (3.17.0)
A whole lot of stuff, but we forgot to update the changelog besides the one entry below, oopsi
- Implemented remote interaction with statuses
## 2022.09 (3.2.0) - 2022-09-10
## 2022.09 - 2022-09-10
### Added
- Automatic post translations. Must be configured on the backend in order to work.
- Post editing, including a log of previous edits.

View file

@ -1,4 +1,4 @@
# Akkoma-FE
# Akkoma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
@ -8,7 +8,7 @@ This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkom
# For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
@ -20,11 +20,9 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
## Build Setup
Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.woodpecker.yml` for which node version the Akkoma CI currently uses.
``` bash
# install dependencies
npm install -g corepack
npm install -g yarn
yarn
# serve with hot reload at localhost:8080
@ -39,7 +37,7 @@ npm run unit
# 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.
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
@ -54,5 +52,4 @@ Edit config.json for configuration.
### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,13 +15,12 @@ put a file that looks like this
```json
{
"myPack": "/static/stickers/myPack/"
"myPack": "/static/stickers/myPack"
}
```
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
it inside `/static/stickers/myPack`". You can add as many packs as you like in this manner.
Note that a single leading and a trailing slash are **required** to work correctly!
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
## Creating the pack

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

@ -6,6 +6,7 @@
<title>Akkoma</title>
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->

View file

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

View file

@ -6,7 +6,6 @@ import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_pan
import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import ModModal from './components/mod_modal/mod_modal.vue'
import SearchFormModal from './components/search_form_modal/search_form_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
@ -36,7 +35,6 @@ export default {
DesktopNav,
SettingsModal,
ModModal,
SearchFormModal,
UserReportingModal,
PostStatusModal,
EditStatusModal,
@ -61,17 +59,11 @@ export default {
{
'-reverse': this.reverseLayout,
'-no-sticky-headers': this.noSticky,
'-has-new-post-button': this.newPostButtonShown,
'-wide-timeline': this.widenTimeline
'-has-new-post-button': this.newPostButtonShown
},
'-' + this.layoutType
]
},
pageBackground () {
return this.mergedConfig.displayPageBackgrounds
? this.$store.state.users.displayBackground
: null
},
currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image },
instanceBackground () {
@ -79,7 +71,7 @@ export default {
? null
: this.$store.state.instance.background
},
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
background () { return this.userBackground || this.instanceBackground },
bgStyle () {
if (this.background) {
return {
@ -96,9 +88,6 @@ export default {
newPostButtonShown () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
widenTimeline () {
return this.$store.getters.mergedConfig.widenTimeline
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType },

View file

@ -8,7 +8,7 @@
}
html {
font-size: 0.875rem;
font-size: 14px;
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
}
@ -172,10 +172,6 @@ nav {
background-color: rgba(0, 0, 0, 0.15);
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
z-index: -1000;
.-wide-timeline & {
margin:0 calc(var(--columnGap) / -2);
}
}
.app-layout {
@ -191,17 +187,12 @@ nav {
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
padding: 0 calc(var(--columnGap) / 2);
align-content: flex-start;
flex-wrap: wrap;
justify-content: center;
min-height: 100vh;
overflow-x: clip;
&.-wide-timeline {
--maxiColumn: minmax(var(--miniColumn), 1fr);
}
.column {
--___columnMargin: var(--columnGap);

View file

@ -62,7 +62,6 @@
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal />
<ModModal />
<SearchFormModal />
<GlobalNoticeList />
</div>
</template>

View file

@ -12,6 +12,7 @@ import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
@ -182,12 +183,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('renderMisskeyMarkdown')
copyInstanceOption('sidebarRight')
if (config.backendCommitUrl)
copyInstanceOption('backendCommitUrl')
if (config.frontendCommitUrl)
copyInstanceOption('frontendCommitUrl')
return store.dispatch('setTheme', config['theme'])
}
@ -252,6 +247,17 @@ const getStickers = async ({ store }) => {
}
}
const getAppSecret = async ({ store }) => {
const { state, commit } = store
const { oauth, instance } = state
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
}
const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
@ -339,7 +345,7 @@ const setConfig = async ({ store }) => {
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig })
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
}
const checkOAuthToken = async ({ store }) => {

View file

@ -1,14 +1,12 @@
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import DMConvTimeline from 'components/dm_conv_timeline/dm_conv_timeline.vue'
import DMConvList from 'components/dm_conv_list/dm_conv_list.vue'
import DMConvRecipients from 'components/dm_conv_recipients/dm_conv_recipients.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import Search from 'components/search/search.vue'
import Registration from 'components/registration/registration.vue'
@ -49,9 +47,6 @@ export default (store) => {
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/main/conversations', component: DMConvList, beforeEnter: validateAuthenticatedRoute },
{ name: 'dm_conversation', path: '/main/conversations/:id', component: DMConvTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'dm-conversation-recipients', path: '/main/conversations/:id/recipients', component: DMConvRecipients },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
@ -67,6 +62,7 @@ export default (store) => {
},
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
@ -76,14 +72,7 @@ export default (store) => {
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'search', path: '/search', component: Search, props: (route) => ({
query: route.query.query,
type: route.query.type,
resolve: route.query.resolve,
account_id: route.query.account_id,
following: route.query.following,
})
},
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About },
{ name: 'lists', path: '/lists', component: Lists },

View file

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

View file

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

View file

@ -54,7 +54,7 @@ const Attachment = {
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false,
img: fileTypeService.fileType(this.attachment) === 'image' && document.createElement('img'),
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false,
showHidden: false,
flashLoaded: false,
@ -105,7 +105,7 @@ const Attachment = {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
},
type () {
return fileTypeService.fileType(this.attachment)
return fileTypeService.fileType(this.attachment.mimetype)
},
hidden () {
return this.nsfw && this.hideNsfwLocal && !this.showHidden

View file

@ -19,17 +19,6 @@
height: 200px;
position: relative;
overflow: hidden;
align-content: center;
.status-popover & {
height: 200px;
}
}
&.-nsfw-placeholder {
.attachment-wrapper {
align-content: unset;
}
}
.description-container {
@ -48,7 +37,7 @@
white-space: pre-line;
word-break: break-word;
text-overflow: ellipsis;
overflow: auto;
overflow: scroll;
}
&.-static {
@ -126,24 +115,6 @@
align-items: center;
justify-content: center;
padding-top: 0.5em;
p {
line-height: 1.5;
padding: 0 0.5em;
white-space: pre-line;
text-align: center;
max-height: 200px;
overflow-y: auto;
scrollbar-color: var(--border) #0000;
width: 100%;
box-sizing: border-box;
.status-popover & {
text-overflow: ellipsis;
overflow: hidden;
height: 1lh;
}
}
}

View file

@ -246,8 +246,8 @@
ref="flash"
class="flash"
:src="attachment.large_thumb_url || attachment.url"
@player-opened="setFlashLoaded(true)"
@player-closed="setFlashLoaded(false)"
@playerOpened="setFlashLoaded(true)"
@playerClosed="setFlashLoaded(false)"
/>
</span>
</div>

View file

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

View file

@ -22,12 +22,12 @@
<script>
export default {
emits: ['update:modelValue'],
props: [
'modelValue',
'indeterminate',
'disabled'
],
emits: ['update:modelValue']
]
}
</script>
@ -69,8 +69,6 @@ export default {
}
&.disabled {
cursor: not-allowed;
.checkbox-indicator::before,
.label {
opacity: .5;

View file

@ -14,7 +14,7 @@
:model-value="present"
:disabled="disabled"
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">
<input
@ -46,6 +46,7 @@
</div>
</div>
</template>
<style lang="scss" src="./color_input.scss"></style>
<script>
import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
@ -107,7 +108,6 @@ export default {
}
}
</script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss">
.color-control {

View file

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

View file

@ -267,11 +267,11 @@ const conversation = {
},
replies () {
let i = 1
// eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
@ -414,14 +414,6 @@ const conversation = {
},
toggleExpanded () {
this.expanded = !this.expanded
const navHeight = document.getElementById("nav").offsetHeight
const headingHeight = document.getElementsByClassName("timeline-heading")[0].offsetHeight
document.documentElement.style.setProperty("--timeline-scroll-margin-top", `${navHeight + headingHeight}px`)
this.$nextTick(() => {
if (!this.expanded) {
this.$el.scrollIntoView({ block: 'nearest' })
}
})
},
getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId]

View file

@ -91,7 +91,7 @@
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
@goto="setHighlight"
@toggle-expanded="toggleExpanded"
@toggleExpanded="toggleExpanded"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -184,7 +184,7 @@
:toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight"
@toggle-expanded="toggleExpanded"
@toggleExpanded="toggleExpanded"
/>
</div>
</div>
@ -278,7 +278,5 @@
&.-expanded.status-fadein {
margin: calc(var(--status-margin, $status-margin) / 2);
}
scroll-margin-block-start: var(--timeline-scroll-margin-top);
}
</style>

View file

@ -1,3 +1,4 @@
import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -46,9 +47,11 @@ library.add(
export default {
components: {
SearchBar,
ConfirmModal
},
data: () => ({
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
@ -75,7 +78,7 @@ export default {
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: 1
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
@ -117,8 +120,8 @@ export default {
scrollToTop () {
window.scrollTo(0, 0)
},
openSearchModal () {
this.$store.dispatch('openSearchModal')
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')

View file

@ -44,9 +44,9 @@
/>
</router-link>
<router-link
v-if="publicTimelineVisible"
:to="{ name: 'public-timeline' }"
class="nav-icon"
v-if="publicTimelineVisible"
>
<FAIcon
fixed-width
@ -68,9 +68,9 @@
/>
</router-link>
<router-link
v-if="federatedTimelineVisible"
:to="{ name: 'public-external-timeline' }"
class="nav-icon"
v-if="federatedTimelineVisible"
>
<FAIcon
fixed-width
@ -96,35 +96,15 @@
>
</router-link>
<div class="item right actions">
<button
<search-bar
v-if="currentUser || !privateMode"
class="button-unstyled nav-icon"
:title="$t('nav.search')"
type="button"
@click.stop="openSearchModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="search"
/>
</button>
@toggled="onSearchBarToggled"
@click.stop
/>
<div
v-if="(currentUser || !privateMode) && showNavShortcuts"
class="nav-items right"
>
<router-link
v-if="currentUser"
class="nav-icon"
:to="{ name: 'dms' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="envelope"
:title="$t('nav.dm_conversations')"
/>
</router-link>
<router-link
v-if="currentUser"
class="nav-icon"

View file

@ -1,80 +0,0 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
const DMConvCard = {
components: {
ConfirmModal,
Status,
UserAvatar
},
props: {
conversation: {
type: Object,
required: true
},
compact: {
type: Boolean,
default: true
},
showLastStatus: {
type: Boolean,
default: true
},
showFullControls: {
type: Boolean,
default: false
}
},
emits: ['deleted'],
data () {
return {
showingDeleteConfirmDialogue: false
}
},
computed: {
shouldConfirmDelete() {
return this.$store.getters.mergedConfig.modalOnDeleteDMConversation;
},
membersTruncated() {
// XXX: this should ideally adapt to panel width
const maxLen = 11
const full = this.conversation.accounts
const truncated = full.length > maxLen
const truncList = truncated ? full.slice(0, maxLen) : full
return {
truncated: truncated,
users: truncList
}
},
last_status_text() {
return this.conversation.last_status?.content
}
},
methods: {
markRead() {
this.$store.dispatch('markDMConversationAsRead', { id: this.conversation.id })
},
showDeleteConfirmModal() {
this.showingDeleteConfirmDialogue = true
},
hideDeleteConfirmModal() {
this.showingDeleteConfirmDialogue = false
},
deleteConversation() {
if (this.shouldConfirmDelete) {
this.showDeleteConfirmModal()
} else {
this.doDeleteConversation()
}
},
doDeleteConversation() {
this.$store.dispatch('deleteDMConversation', { id: this.conversation.id })
this.hideDeleteConfirmModal()
this.$emit('deleted')
}
}
}
export default DMConvCard

View file

@ -1,134 +0,0 @@
<template>
<div class="dm-conv-card">
<router-link
:to="{ name: 'dm_conversation', params: {id: conversation.id }}"
>
<div class="heading">
<div class="title-bar">
<div class="title-bar-left">
<div
v-if="conversation.unread"
class="unread"
>
<span
class="badge badge-notification"
role="figure"
:title="$t('dm_conv.unread_msg')"
:alt="$t('dm_conv.unread_msg')"
>
!
</span>
<button
class="button-unstyled"
:title="$t('dm_conv.mark_single_read_tooltip')"
@click.stop.prevent="markRead()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding dm-conv-mark-read"
/>
</button>
&nbsp;
</div>
<h4>{{ $t('dm_conv.default_name', {id: conversation.id}) }}</h4>
</div>
<div class="title-bar-right">
<button
class="button-unstyled button-delete"
:title="$t('dm_conv.delete_tooltip')"
@click.stop.prevent="deleteConversation()"
>
<FAIcon
icon="trash-alt"
class="fa-scale-110 fa-old-padding dm-conv-delete"
/>
</button>
</div>
</div>
</div>
<div class="members">
<UserAvatar
v-for="user in membersTruncated.users"
:key="user.id"
:user="user"
:compact="compact"
/>
<div
v-if="membersTruncated.truncated"
class="ellipsis"
>
...
</div>
</div>
</router-link>
<div
v-if="showLastStatus"
class="last-message"
>
<div class="last-message-title">
{{ $t('dm_conv.last_message_title') }}:
</div>
<Status
:statusoid="conversation.last_status"
:compact="true"
:is-preview="true"
/>
</div>
<div
v-if="showFullControls"
class="controls"
>
<button
class="btn button-default"
:title="$t('dm_conv.recipients_edit_mode_button_tooltip')"
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: conversation.id }})"
>
{{ $t('dm_conv.recipients_edit_mode_button') }}
</button>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingDeleteConfirmDialogue"
:title="$t('dm_conv.delete_confirm_title')"
:confirm-text="$t('dm_conv.delete_confirm_accept_button')"
:cancel-text="$t('dm_conv.delete_confirm_cancel_button')"
@accepted="doDeleteConversation"
@cancelled="hideDeleteConfirmModal"
>
{{ $t('dm_conv.delete_confirm', { identifier: conversation.id }) }}
</confirm-modal>
</teleport>
</div>
</template>
<script src="./dm_conv_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.dm-conv-card {
.heading, .title-bar, .title-bar-left, .members {
display: flex;
flex-wrap: nowrap;
overflow-x: hidden;
}
.title-bar {
width: 100%;
justify-content: space-between;
}
.controls {
text-align: center;
}
.members {
padding: 6px 0;
}
.last-message-title {
font-style: italic;
color: var(--faint);
}
}
</style>

View file

@ -1,33 +0,0 @@
import DMConvCard from '../dm_conv_card/dm_conv_card.vue'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
const PaginatedDMConvList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchDMConversationList'),
select: (props, $store) => $store.state.dmConversations.allDMConversations || [],
destroy: (props, $store) => $store.dispatch('clearDMConversations'),
childPropName: 'items',
additionalPropNames: []
})(List)
const DMConvList = {
components: {
PaginatedDMConvList,
DMConvCard
},
data () {
return {}
},
computed: {
conversations() {
return this.$store.state.dmConversations.allDMConversations
}
},
methods: {
markAllRead() {
this.$store.dispatch('markAllDMConversationsAsRead')
}
}
}
export default DMConvList

View file

@ -1,47 +0,0 @@
<template>
<div class="settings panel panel-default dm-conv-panel">
<div class="panel-heading">
<div class="title">
{{ $t('nav.dm_conv_list') }}
</div>
</div>
<div class="panel-controls">
<button
class="btn button-default mark-all-read-button"
@click="markAllRead()"
>
{{ $t('dm_conv.mark_all_read_button') }}
</button>
</div>
<div class="panel-body dm-conv-list">
<PaginatedDMConvList>
<template #item="{item}">
<DMConvCard :conversation="item" />
</template>
</PaginatedDMConvList>
</div>
</div>
</template>
<script src="./dm_conv_list.js"></script>
<style lang="scss">
.dm-conv-panel {
.dm-conv-list {
margin: 0 1em;
.dm-conv-card {
margin: 2.5em 0;
}
}
.panel-controls {
margin-top: 0.5em;
text-align: center;
}
.mark-all-read-button {
display: inline-block;
}
}
</style>

View file

@ -1,57 +0,0 @@
import { mapGetters } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ListUserSearch from '../list_user_search/list_user_search.vue'
const DMConvRecipients = {
data () {
return {
conversationId: null,
conversation: null,
recipients: [],
suggestions: []
}
},
components: {
BasicUserCard,
ListUserSearch
},
computed: {
conversationTitle () {
return this.$i18n.t('dm_conv.default_name', {id: this.conversationId})
},
...mapGetters(['findUser'])
},
methods: {
toggleUser (user) {
if (this.isRecipient(user)) {
this.recipients.filter((r) => r.id !== user.id)
} else {
this.recipients.push(user)
}
},
isRecipient (user) {
return this.recipients.some((r) => r.id == user.id)
},
onResults (results) {
this.suggestions = results.map((id) => this.findUser(id)).filter(user => user)
},
updateRecipients () {
const recipientIds = this.recipients.map((u) => u.id)
this.$store.dispatch('setDMConversationDetails', {id: this.conversationId, recipients: recipientIds })
.then((updateConv) => {
this.conversation = updateConv
this.recipients = updateConv.accounts
})
}
},
created () {
this.conversationId = this.$route.params.id
this.$store.dispatch('fetchDMConversationDetails', { id: this.conversationId })
.then((data) => {
this.conversation = data
this.recipients = data.accounts
})
},
}
export default DMConvRecipients

View file

@ -1,82 +0,0 @@
<template>
<div class="panel-default panel dm-conv-recipients-edit">
<div
ref="header"
class="panel-heading"
>
<div class="title">
{{ $t('dm_conv.recipients_edit_title', {conversation_name: conversationTitle}) }}
</div>
<button
class="btn button-default"
@click="$router.back"
>
{{ $t('nav.back') }}
</button>
</div>
<h4>
{{ $t('dm_conv.recipients_edit_current_title') }}
</h4>
<div class="member-list current-recipients">
<div
v-for="user in recipients"
:key="user.id"
class="member"
>
<BasicUserCard
:user="user"
class="selected"
@click.capture.prevent="toggleUser(user)"
/>
</div>
</div>
<h4>
{{ $t('dm_conv.recipients_edit_add_new_title') }}
</h4>
<ListUserSearch @results="onResults" />
<div class="member-list">
<div
v-for="user in suggestions"
:key="user.id"
class="member"
>
<BasicUserCard
:user="user"
:class="isRecipient(user) ? 'selected' : ''"
@click.capture.prevent="toggleUser(user)"
/>
</div>
</div>
<button
class="btn button-default"
@click="updateRecipients"
>
{{ $t('dm_conv.recipients_save') }}
</button>
</div>
</template>
<script src="./dm_conv_recipients.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.dm-conv-recipients-edit {
.member-list {
padding-bottom: 0.7rem;
}
.current-recipients {
margin-bottom: 1.5em;
}
.basic-user-card:hover,
.basic-user-card.selected {
cursor: pointer;
background-color: var(--selectedPost, $fallback--lightBg);
}
}
</style>

View file

@ -1,34 +0,0 @@
import DMConvCard from '../dm_conv_card/dm_conv_card.vue'
import Timeline from '../timeline/timeline.vue'
const DMConvTimeline = {
data () {
return {
conversationId: null
}
},
components: {
DMConvCard,
Timeline
},
computed: {
conversation () { return this.$store.getters.getDMConversationById(this.conversationId) },
timeline () { return this.$store.state.statuses.timelines.dmConv }
},
methods: {
forceLeave () {
this.$router.push('/')
}
},
created () {
this.conversationId = this.$route.params.id
this.$store.dispatch('fetchDMConversationDetails', { id: this.conversationId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'dmConv', conversationId: this.conversationId })
},
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'dmConv')
this.$store.commit('clearTimeline', { timeline: 'dmConv' })
}
}
export default DMConvTimeline

View file

@ -1,24 +0,0 @@
<template>
<Timeline
title="$t('dm_conv.page_header')"
:timeline="timeline"
:conversation-id="conversationId"
timeline-name="dmConv"
>
<template
#extraHeading
>
<DMConvCard
v-if="conversation"
:conversation="conversation"
:compact="false"
:show-full-controls="true"
:show-last-status="false"
:link-to-timeline="false"
@deleted="forceLeave"
/>
</template>
</Timeline>
</template>
<script src="./dm_conv_timeline.js"></script>

View file

@ -0,0 +1,14 @@
import Timeline from '../timeline/timeline.vue'
const DMs = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.dms
}
},
components: {
Timeline
}
}
export default DMs

View file

@ -0,0 +1,9 @@
<template>
<Timeline
:title="$t('nav.dms')"
:timeline="timeline"
:timeline-name="'dms'"
/>
</template>
<script src="./dm_timeline.js"></script>

View file

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

View file

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

View file

@ -1,5 +1,3 @@
import StillImage from '../still-image/still-image.vue'
const EMOJI_SIZE = 32 + 8
const GROUP_TITLE_HEIGHT = 24
const BUFFER_SIZE = 3 * EMOJI_SIZE
@ -19,9 +17,6 @@ const EmojiGrid = {
resizeObserver: null
}
},
components: {
StillImage
},
mounted () {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width

View file

@ -34,11 +34,10 @@
@click.stop.prevent="onEmoji(item.emoji)"
>
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
<StillImage
<img
v-else
:src="item.emoji.imageUrl"
no-stop-gifs="true"
/>
>
</span>
</template>
</div>

View file

@ -1,6 +1,5 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import StillImage from '../still-image/still-image.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -121,8 +120,7 @@ const EmojiInput = {
}
},
components: {
EmojiPicker,
StillImage
EmojiPicker
},
computed: {
padEmoji () {

View file

@ -20,7 +20,6 @@
ref="picker"
show-keep-open
:class="{ hide: !showPicker }"
:visible="showPicker"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"
@emoji="insert"
@ -44,15 +43,11 @@
:class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
>
<span
v-if="!suggestion.mfm"
class="image"
>
<StillImage
<span v-if="!suggestion.mfm" class="image">
<img
v-if="suggestion.img"
:src="suggestion.img"
no-stop-gifs="true"
/>
>
<span v-else>{{ suggestion.replacement }}</span>
</span>
<div class="label">

View file

@ -1,4 +1,4 @@
const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
/**
@ -71,7 +71,7 @@ export const suggestUsers = ({ dispatch, state }) => {
let timeout = null
let cancelUserSearch = null
const userSearch = (query) => dispatch('searchUsers', { query, resolve: false })
const userSearch = (query) => dispatch('searchUsers', { query })
const debounceUserSearch = (query) => {
cancelUserSearch && cancelUserSearch()
return new Promise((resolve, reject) => {
@ -86,20 +86,19 @@ export const suggestUsers = ({ dispatch, state }) => {
}
return async input => {
if (previousQuery === input) return suggestions
const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions
suggestions = []
previousQuery = input
// if there are more than two @, its not a valid nick
if (input.match(/@/g)?.length > 2) {
return []
previousQuery = noPrefix
// Fetch more and wait, don't fetch if there's the 2nd @ because
// the backend user search can't deal with it.
// Reference semantics make it so that we get the updated data after
// the await.
if (!noPrefix.includes('@')) {
await debounceUserSearch(noPrefix)
}
// fetch new matching users into our cache
await debounceUserSearch(input)
const noPrefix = input.toLowerCase().substr(1)
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
@ -123,14 +122,14 @@ export const suggestUsers = ({ dispatch, state }) => {
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
displayText: screen_name_ui,
detailText: name,
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
/* eslint-enable camelcase */
suggestions = newSuggestions || []
return suggestions

View file

@ -1,7 +1,6 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
@ -27,17 +26,12 @@ const EmojiPicker = {
required: false,
type: Boolean,
default: false
},
visible: {
required: false,
type: Boolean,
default: true
}
},
data () {
return {
keyword: '',
activeGroup: this.getDefaultGroup(),
activeGroup: 'standard',
showingStickers: false,
keepOpen: false
}
@ -45,8 +39,7 @@ const EmojiPicker = {
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
EmojiGrid,
StillImage
EmojiGrid
},
methods: {
debouncedSearch: debounce(function (e) {
@ -89,11 +82,6 @@ const EmojiPicker = {
return list.filter(emoji => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
})
},
getDefaultGroup () {
if (!this.visible) return null
const recentEmojis = this.$store.getters.recentEmojis
return recentEmojis.length === 0 ? 'standard' : 'recent'
}
},
computed: {
@ -160,13 +148,6 @@ const EmojiPicker = {
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
}
},
watch: {
visible (val, oldVal) {
if (val && this.activeGroup === null) {
this.activeGroup = this.getDefaultGroup()
}
}
}
}

View file

@ -18,11 +18,10 @@
@click.prevent="highlight(group.id)"
>
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
<StillImage
<img
v-else
:src="group.first.imageUrl"
no-stop-gifs="true"
/>
>
</span>
<span
v-if="stickerPickerEnabled"

View file

@ -11,7 +11,7 @@
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<template
<span
v-if="reaction.url !== null"
>
<StillImage
@ -19,15 +19,16 @@
:title="reaction.name"
:alt="reaction.name"
class="reaction-emoji"
height="2.55em"
/>
{{ reaction.count }}
</template>
<template v-else>
</span>
<span v-else>
<span class="reaction-emoji unicode-emoji">
{{ reaction.name }}
</span>
<span>{{ reaction.count }}</span>
</template>
</span>
</button>
</UserListPopover>
<a
@ -41,7 +42,7 @@
</div>
</template>
<script src="./emoji_reactions.js"></script>
<script src="./emoji_reactions.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
@ -52,26 +53,23 @@
container-type: inline-size;
}
.unicode-emoji {
font-size: 210%;
}
.emoji-reaction {
padding: 2px 0.5em;
padding: 0 0.5em;
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
align-items: end;
align-items: center;
justify-content: center;
box-sizing: border-box;
.reaction-emoji {
width: auto;
max-width: 96cqw;
height: 2.55em !important;
margin-right: 0.25em;
&.still-image {
height: 2.55em;
}
&.unicode-emoji {
display: inline-block;
font-size: 2.125em; // assuming default line height of 1.2rem and emojis that don't exceed line height
line-height: 2.55rem;
}
}
&:focus {
outline: none;
@ -99,9 +97,9 @@
}
.button-default.picked-reaction {
&, &:hover {
box-shadow: inset 0 0 0 1px var(--accent, $fallback--link);
}
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
</style>

View file

@ -91,7 +91,7 @@ const ExtraButtons = {
.catch(err => this.$emit('onError', err.error.error))
},
copyLink () {
navigator.clipboard.writeText(this.status.canonical_id)
navigator.clipboard.writeText(this.statusLink)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
@ -155,9 +155,8 @@ const ExtraButtons = {
replyTo: this.status.in_reply_to_status_id,
repliedUser: repliedUser
})
}).then(() => {
this.doDeleteStatus()
})
this.doDeleteStatus()
},
showRedraftStatusConfirmDialog () {
this.showingRedraftDialog = true
@ -188,6 +187,13 @@ const ExtraButtons = {
noTranslationTargetSet () {
return this.$store.getters.mergedConfig.translationLanguage === undefined
},
statusLink () {
if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
} else {
return this.status.external_url
}
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},

View file

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

View file

@ -19,31 +19,15 @@ const FavoriteButton = {
},
methods: {
favorite () {
const undoing = this.status.favorited
let action
if (!undoing) {
action = this.$store.dispatch('favorite', { id: this.status.id })
if (!this.status.favorited) {
this.$store.dispatch('favorite', { id: this.status.id })
} else {
action = this.$store.dispatch('unfavorite', { id: this.status.id })
this.$store.dispatch('unfavorite', { id: this.status.id })
}
this.animated = true
action.then(() => {
setTimeout(() => {
this.animated = false
this.$store.dispatch('fetchFavs', this.status.id)
})
.catch((error) => {
this.$store.dispatch('pushGlobalNotice', {
level: 'error',
messageKey: undoing ? 'errors.favorite_undo' : 'errors.favorite',
messageArgs: [error.message ?? error ?? 'unknown'],
timeout: 5000
})
})
.finally(() => {
this.animated = false
})
}, 500)
}
},
computed: {

View file

@ -9,7 +9,7 @@
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:icon="[(status.favorited || animated) ? 'fas' : 'far', 'star']"
:icon="[status.favorited ? 'fas' : 'far', 'star']"
:spin="animated"
/>
</button>
@ -35,7 +35,7 @@
</div>
</template>
<script src="./favorite_button.js"></script>
<script src="./favorite_button.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
@ -61,10 +61,6 @@
animation-duration: 0.6s;
}
.svg-inline--fa.fa-spin {
color: color-mix(in srgb-linear, var(--cOrange, $fallback--cOrange) 70%, currentColor);
}
&:hover .svg-inline--fa,
&.-favorited .svg-inline--fa {
color: $fallback--cOrange;

View file

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

View file

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

View file

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

View file

@ -88,8 +88,10 @@ const Gallery = {
set(this.sizes, id, { width, height })
},
rowStyle (row) {
if (!row.audio && !row.minimal && !row.grid) {
return { 'aspect-ratio': `1/${(1 / (row.items.length + 0.6))}` }
if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
}
},
itemStyle (id, row) {

View file

@ -31,8 +31,8 @@
:description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
:style="itemStyle(attachment.id, row.items)"
@set-media="onMedia"
@natural-size-load="onNaturalSizeLoad"
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
</div>
</div>
@ -96,15 +96,9 @@
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
.Status & {
max-height: 30em;
}
&.-audio {
aspect-ratio: 4/1; // this is terrible, but it's how it was before so I'm not changing it >:(
}
&:not(:first-child) {
margin-top: 0.5em;

View file

@ -14,6 +14,6 @@
</span>
</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>
</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';
.list {
min-height: 1em;
&-item:not(:last-child) {
border-bottom: 1px solid;
border-bottom-color: $fallback--border;

View file

@ -22,17 +22,13 @@ const ListNew = {
data () {
return {
title: '',
exclusive: false,
userIds: [],
selectedUserIds: []
}
},
created () {
this.$store.dispatch('fetchList', { id: this.id })
.then((list) => {
this.title = list.title
this.exclusive = !!list.exclusive
})
.then(() => { this.title = this.findListTitle(this.id) })
this.$store.dispatch('fetchListAccounts', { id: this.id })
.then(() => {
this.selectedUserIds = this.findListAccounts(this.id)
@ -80,7 +76,7 @@ const ListNew = {
this.userIds = results
},
updateList () {
this.$store.dispatch('setList', { id: this.id, title: this.title, exclusive: this.exclusive })
this.$store.dispatch('setList', { id: this.id, title: this.title })
this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds })
this.$router.push({ name: 'list-timeline', params: { id: this.id } })

View file

@ -21,17 +21,6 @@
:placeholder="$t('lists.title')"
>
</div>
<div class="input-wrap">
<input
type="checkbox"
id="list-exclusive-input"
ref="exclusive"
v-model="exclusive"
>
<label for="list-exclusive-input">
{{ $t('lists.exclusive_description') }}
</label>
</div>
<div class="member-list">
<div
v-for="user in selectedUsers"

View file

@ -22,7 +22,6 @@ const ListNew = {
data () {
return {
title: '',
exclusive: false,
userIds: [],
selectedUserIds: []
}
@ -68,7 +67,7 @@ const ListNew = {
createList () {
// the API has two different endpoints for "creating a list with a name"
// and "updating the accounts on the list".
this.$store.dispatch('createList', { title: this.title, exclusive: this.exclusive })
this.$store.dispatch('createList', { title: this.title })
.then((list) => {
this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds })
this.$router.push({ name: 'list-timeline', params: { id: list.id } })

View file

@ -21,17 +21,6 @@
:placeholder="$t('lists.title')"
>
</div>
<div class="input-wrap">
<input
type="checkbox"
id="list-exclusive-input"
ref="exclusive"
v-model="exclusive"
>
<label for="list-exclusive-input">
{{ $t('lists.exclusive_description') }}
</label>
</div>
<div class="member-list">
<div

View file

@ -35,7 +35,7 @@ const ListUserSearch = {
this.loading = true
this.userIds = []
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
.then(({data}) => {
.then(data => {
this.loading = false
this.$emit('results', data.accounts.map(a => a.id))
})

View file

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

View file

@ -43,7 +43,7 @@ const LoginForm = {
}
oauthApi.getOrCreateApp(data)
.then((app) => { oauthApi.login({ ...data, ...app }) })
.then((app) => { oauthApi.login({ ...app, ...data }) })
},
submitPassword () {
const { clientId } = this.oauth

View file

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

View file

@ -67,7 +67,7 @@ const MediaModal = {
},
methods: {
getType (media) {
return fileTypeService.fileType(media)
return fileTypeService.fileType(media.mimetype)
},
hide () {
// HACK: Closing immediately via a touch will cause the click

View file

@ -2,7 +2,7 @@
<Modal
v-if="showing"
class="media-modal-view"
@backdrop-clicked="hideIfNotSwiped"
@backdropClicked="hideIfNotSwiped"
>
<SwipeClick
v-if="type === 'image'"
@ -24,15 +24,14 @@
:min-scale="pinchZoomMinScale"
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
>
<StillImage
<img
:class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
:image-load-handler="onImageLoaded"
no-stop-gifs="true"
/>
@load="onImageLoaded"
>
</PinchZoom>
</SwipeClick>
<VideoAttachment

View file

@ -42,14 +42,8 @@ const mediaUpload = {
.then((fileData) => {
self.$emit('uploaded', fileData)
self.decreaseUploadCount()
}, (error) => {
var msg = typeof error === "string" ? error : error.message
if (msg) {
self.$emit('upload-failed', 'message', [msg])
} else {
self.$emit('upload-failed', 'default')
}
console.warn(`Failed to upload media: ${error}`)
}, (error) => { // eslint-disable-line handle-callback-err
self.$emit('upload-failed', 'default')
self.decreaseUploadCount()
})
},

View file

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

View file

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

View file

@ -66,6 +66,6 @@
</span>
</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>
</template>
<script src="./mentions_line.js"></script>
<script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,9 +53,6 @@ const NavPanel = {
federating: state => state.instance.federating,
}),
...mapGetters(['unreadAnnouncementCount']),
unreadDMConversationsCount () {
return this.$store.state.users.currentUser?.pleroma?.unread_conversation_count || 0
},
followRequestCount () {
return this.$store.state.users.currentUser.follow_requests_count
}

View file

@ -25,24 +25,6 @@
<TimelineMenuContent class="timelines" />
</div>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="envelope"
/>{{ $t("nav.dm_conversations") }}
<span
v-if="unreadDMConversationsCount > 0"
class="badge badge-notification"
>
{{ unreadDMConversationsCount }}
</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
@ -120,7 +102,7 @@
</div>
</template>
<script src="./nav_panel.js"></script>
<script src="./nav_panel.js" ></script>
<style lang="scss">
@import '../../_variables.scss';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,6 @@
import Timeago from 'components/timeago/timeago.vue'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash'
import {
faCircleCheck,
faTriangleExclamation
} from '@fortawesome/free-solid-svg-icons'
export default {
name: 'Poll',
@ -16,9 +12,6 @@ export default {
data () {
return {
loading: false,
peekingResults: false,
collapsable: false,
collapsed: false,
choices: []
}
},
@ -28,10 +21,6 @@ export default {
}
this.$store.dispatch('trackPoll', this.pollId)
},
mounted () {
this.collapsable = this.$refs.pollContainer?.scrollHeight > this.$refs.pollContainer?.clientHeight
this.collapsed = this.collapsable
},
unmounted () {
this.$store.dispatch('untrackPoll', this.pollId)
},
@ -55,31 +44,15 @@ export default {
loggedIn () {
return this.$store.state.users.currentUser
},
canVote () {
return !(this.poll.voted || this.expired || !this.loggedIn)
},
showResults () {
return !this.canVote || this.peekingResults
return this.poll.voted || this.expired || !this.loggedIn
},
totalVotesCount () {
return this.poll.votes_count
},
totalFractionBase () {
// Due to a backend bug, we might not have any voter count info for remote polls
// in this case, fall back to count of votes even for multiple cjoice polls
// to be able to at least display _something_
const total_base = this.poll.multiple ? this.poll.voters_count : this.poll.votes_count
return total_base > 0 ? total_base : this.poll.votes_count
},
maxHeight () {
// keep in sync with CSS!
return 330; // in px
},
containerClasses () {
containerClass () {
return {
loading: this.loading,
collapsable: this.collapsable,
collapsed: this.collapsed
loading: this.loading
}
},
choiceIndices () {
@ -97,11 +70,10 @@ export default {
},
methods: {
percentageForOption (count) {
const total = this.totalFractionBase
return total === 0 ? 0 : Math.round(count / total * 100)
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
resultTitle (option) {
return `${option.votes_count}/${this.totalFractionBase} ${this.$t('polls.votes')}`
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
@ -130,12 +102,6 @@ export default {
optionId (index) {
return `poll${this.poll.id}-${index}`
},
peekResults (peeking) {
this.peekingResults = peeking
},
collapse(collapsed) {
this.collapsed = collapsed
},
vote () {
if (this.choiceIndices.length === 0) return
this.loading = true

View file

@ -1,140 +1,88 @@
<template>
<div
ref="pollContainer"
class="poll"
:class="containerClasses"
:class="containerClass"
>
<button
v-show="collapsable && !collapsed"
class="show-less-button button-unstyled -link"
@click.stop.prevent="collapse(true)"
<div
v-for="(option, index) in options"
:key="index"
class="poll-option"
>
{{ $t('polls.show_less') }}
</button>
<button
v-show="collapsed"
class="show-more-button button-unstyled -link"
@click.stop.prevent="collapse(false)"
>
{{ $t('polls.show_full') }}
</button>
<div class="poll-content">
<div
v-for="(option, index) in options"
:key="index"
class="poll-option"
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
>
<div
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
>
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
/>
</div>
<div
v-else
@click="activateOption(index)"
>
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
@click="activateOption(index)"
type="radio"
:disabled="loading"
:value="index"
>
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
>
<label class="option-vote">
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</div>
<label class="option-vote">
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</div>
<div class="poll-hint">
<div
v-if="poll.akkoma?.anonymous === true"
class="alert success"
>
<FAIcon icon="check-circle" />
&nbsp;
{{ $t('polls.indicate_anonymous') }}
</div>
<div
v-else-if="poll.akkoma?.anonymous === false"
class="alert warning"
>
<FAIcon icon="triangle-exclamation" />
&nbsp;
{{ $t('polls.indicate_disclosure') }}
</div>
</div>
<div class="footer faint">
<button
v-if="!showResults"
class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"
>
{{ $t('polls.vote') }}
</button>
<div class="total">
<template v-if="typeof poll.voters_count === 'number'">
{{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp;
</template>
<template v-else>
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template>
</div>
<div class="footer faint">
<button
v-if="!showResults"
class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"
<span>
<i18n-t
scope="global"
:keypath="expired ? 'polls.expired' : 'polls.expires_in'"
>
{{ $t('polls.vote') }}
</button>
<button
v-if="!showResults"
class="btn button-default poll-results-toggle-button"
type="button"
:disabled="!isDisabled"
@click="peekResults(true)"
>
{{ $t('polls.show_results') }}
</button>
<button
v-else-if="canVote"
class="btn button-default poll-results-toggle-button"
type="button"
@click="peekResults(false)"
>
{{ $t('polls.hide_results') }}
</button>
<div class="total">
<template v-if="typeof poll.voters_count === 'number'">
{{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp;
</template>
<template v-else>
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template>
</div>
<span>
<i18n-t
scope="global"
:keypath="expired ? 'polls.expired' : 'polls.expires_in'"
>
<Timeago
:time="expiresAt"
:auto-update="60"
:now-threshold="0"
/>
</i18n-t>
</span>
</div>
<Timeago
:time="expiresAt"
:auto-update="60"
:now-threshold="0"
/>
</i18n-t>
</span>
</div>
</div>
</template>
@ -145,58 +93,13 @@
@import '../../_variables.scss';
.poll {
// keep in sync with js!
max-height: 330px;
overflow-y: hide;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
&.collapsable:not(.collapsed) {
max-height: unset;
}
&.collapsed {
.poll-content {
mask: linear-gradient(
to bottom,
white 260px,
transparent 330px
);
}
}
.poll-content {
padding-top: 0.375em;
}
.show-less-button, .show-more-button {
z-index: 2;
display: inline-block;
width: 100%;
text-align: center;
}
.show-less-button {
position: relative;
}
.show-more-button {
position: absolute;
margin-top: 260px;
height: 70px;
line-height: 110px;
}
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
margin: 0.375em 0.5em;
margin: 0.75em 0.5em;
}
.option-result {
height: 100%;
@ -241,13 +144,10 @@
display: flex;
align-items: center;
}
.poll-hint {
margin: 0.25em 0;
}
&.loading * {
cursor: progress;
}
.poll-vote-button, .poll-results-toggle-button {
.poll-vote-button {
padding: 0 0.5em;
margin-right: 0.5em;
}

View file

@ -24,7 +24,6 @@
<button
v-if="options.length > 2"
class="delete-option button-unstyled -hover-highlight"
type="button"
@click="deleteOption(index)"
>
<FAIcon icon="times" />
@ -33,7 +32,6 @@
<button
v-if="options.length < maxOptions"
class="add-option faint button-unstyled -hover-highlight"
type="button"
@click="addOption"
>
<FAIcon

View file

@ -9,13 +9,11 @@ import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language'
import scopeUtils from 'src/lib/scope_utils.js'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -64,13 +62,6 @@ const deleteDraft = (draftKey) => {
localStorage.setItem('drafts', JSON.stringify(draftData));
}
const interfaceToISOLanguage = (ilang) => {
const sep = ilang.indexOf("_");
return sep < 0 ?
ilang :
ilang.substr(0, sep);
}
const PostStatusForm = {
props: [
'statusId',
@ -86,7 +77,6 @@ const PostStatusForm = {
'quoteId',
'repliedUser',
'attentions',
'copyMessageLanguage',
'copyMessageScope',
'subject',
'disableSubject',
@ -139,23 +129,16 @@ const PostStatusForm = {
this.$refs.textarea.focus()
}
},
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {
postLanguageOptions,
}
},
data () {
const preset = this.$route.query.message
let statusText = preset || ''
if (this.replyTo || this.quoteId || this.repliedUser) {
if (this.replyTo || this.quoteId) {
const currentUser = this.$store.state.users.currentUser
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
@ -166,7 +149,7 @@ const PostStatusForm = {
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: this.suggestedLanguage(),
language: interfaceLanguage,
contentType
}
@ -181,7 +164,7 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || this.suggestedLanguage(),
language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType
}
}
@ -216,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 {
dropFiles: [],
uploadingFiles: false,
@ -234,10 +213,7 @@ const PostStatusForm = {
preview: null,
previewLoading: false,
emojiInputShown: false,
idempotencyKey: '',
activeEmojiInput: undefined,
activeTextInput: undefined,
subjectVisible: showSubject
idempotencyKey: ''
}
},
computed: {
@ -326,11 +302,13 @@ const PostStatusForm = {
...mapState({
mobileLayout: state => state.interface.mobileLayout
}),
isoLanguages () {
return iso6391.getAllCodes();
}
},
watch: {
'newStatus': {
deep: true,
flush: 'sync',
handler () {
this.statusChanged()
}
@ -343,22 +321,17 @@ const PostStatusForm = {
this.saveDraft()
},
clearStatus () {
const config = this.$store.getters.mergedConfig
const newStatus = this.newStatus
this.newStatus = {
status: '',
spoilerText: '',
files: [],
nsfw: !!config.sensitiveByDefault,
visibility: this.suggestedVisibility(),
contentType: config.postContentType,
language: this.suggestedLanguage(),
visibility: newStatus.visibility,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
mediaDescriptions: {}
}
const scopeselector = this.$refs.scopeselector
if (scopeselector) {
scopeselector.currentScope = this.newStatus.visibility
}
this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
this.clearPollForm()
@ -518,7 +491,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '' || !!this.$store.getters.mergedConfig.sensitiveByDefault) {
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
this.newStatus.nsfw = true
}
this.$emit('resize', { delayed: true })
@ -555,7 +528,7 @@ const PostStatusForm = {
this.uploadingFiles = false
},
type (fileInfo) {
return fileTypeService.fileType(fileInfo)
return fileTypeService.fileType(fileInfo.mimetype)
},
paste (e) {
this.autoPreview()
@ -701,33 +674,8 @@ const PostStatusForm = {
this.$refs['emoji-input'].resize()
},
showEmojiPicker () {
if (!this.activeEmojiInput || !this.activeTextInput)
this.focusStatusInput()
this.$refs[this.activeTextInput].focus()
this.$refs[this.activeEmojiInput].triggerShowPicker()
},
focusStatusInput() {
this.activeEmojiInput = 'emoji-input'
this.activeTextInput = 'textarea'
},
focusSubjectInput() {
this.activeEmojiInput = 'subject-emoji-input'
this.activeTextInput = 'subject-input'
},
toggleSubjectVisible() {
// If hiding CW, then we need to clear the subject and reset focus
if (this.subjectVisible)
{
this.focusStatusInput()
// "nsfw" property is normally set by the @change listener, but this bypasses it.
// We need to clear it manually instead.
this.newStatus.spoilerText = ''
this.newStatus.nsfw = false
}
this.subjectVisible = !this.subjectVisible
this.$refs['textarea'].focus()
this.$refs['emoji-input'].triggerShowPicker()
},
clearError () {
this.error = null
@ -767,19 +715,16 @@ const PostStatusForm = {
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
suggestedLanguage () {
// Make sure the inherited language is actually valid
if (this.postLanguageOptions.find(o => o.value === this.copyMessageLanguage)) {
return this.copyMessageLanguage
}
const { postLanguage: defaultPostLanguage, interfaceLanguage } = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
return postLanguage
},
suggestedVisibility () {
const maxScope = this.copyMessageScope
const defaultScope = this.$store.state.users.currentUser.default_scope
return scopeUtils.negotiate(defaultScope, maxScope)
if (this.copyMessageScope) {
if (this.copyMessageScope === 'direct') {
return this.copyMessageScope
}
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope
}
}
return this.$store.state.users.currentUser.default_scope
}
}
}

View file

@ -18,7 +18,6 @@
>
<button
class="button-unstyled -link"
type="button"
@click="openProfileTab"
>
{{ $t('post_status.account_not_locked_warning_link') }}
@ -119,16 +118,13 @@
/>
</div>
<EmojiInput
v-if="subjectVisible"
ref="subject-emoji-input"
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
v-model="newStatus.spoilerText"
enable-emoji-picker
hide-emoji-button
:suggest="emojiSuggestor"
class="form-control"
>
<input
ref="subject-input"
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
@ -136,8 +132,6 @@
size="1"
class="form-post-subject"
@input="onSubjectInput"
@focus="focusSubjectInput()"
@keydown.exact.enter.prevent
>
</EmojiInput>
<i18n-t
@ -172,14 +166,13 @@
cols="1"
:disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }"
:class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize"
@compositionupdate="resize"
@paste="paste"
@focus="focusStatusInput()"
/>
<p
v-if="hasStatusLengthLimit"
@ -192,11 +185,9 @@
<div
v-if="!disableScopeSelector"
class="visibility-tray"
:class="{ 'visibility-tray-edit': isEdit }"
>
<scope-selector
v-if="!disableVisibilitySelector"
ref="scopeselector"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
:initial-scope="newStatus.visibility"
@ -204,51 +195,47 @@
/>
<div
class="format-selector-container"
class="language-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in isoLanguages"
:key="language"
:value="language"
>
{{ language }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format"
>
<div
class="format-selector"
<Select
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
<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
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 format-selector"
>
<span class="only-format">
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
</span>
</div>
{{ $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>
@ -276,7 +263,6 @@
<button
class="emoji-icon button-unstyled"
:title="$t('emoji.add_emoji')"
type="button"
@click="showEmojiPicker"
>
<FAIcon icon="smile-beam" />
@ -286,21 +272,10 @@
class="poll-icon button-unstyled"
:class="{ selected: pollFormVisible }"
:title="$t('polls.add_poll')"
type="button"
@click="togglePollForm"
>
<FAIcon icon="poll-h" />
</button>
<button
v-if="!disableSubject"
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
type="button"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />
</button>
</div>
<button
v-if="posting"
@ -471,10 +446,6 @@
align-items: baseline;
}
.visibility-tray-edit {
justify-content: right;
}
.visibility-notice.edit-warning {
> :first-child {
margin-top: 0;
@ -485,13 +456,7 @@
}
}
.format-selector-container {
.format-selector {
display: inline-block;
}
}
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
.media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em;
line-height: 1.1;
flex: 1;
@ -534,11 +499,6 @@
.poll-icon {
order: 3;
justify-content: center;
}
.spoiler-icon {
order: 4;
justify-content: right;
}
@ -591,11 +551,6 @@
line-height: 1.85;
}
.form-post-subject {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-post-body {
// TODO: make a resizable textarea component?
box-sizing: content-box; // needed for easier computation of dynamic size
@ -608,11 +563,6 @@
min-height: calc(var(--post-line-height) * 1em);
resize: none;
&.-has-subject {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&.scrollable-form {
overflow-y: auto;
}

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