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
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
build/*.js
|
||||
config/*.js
|
||||
30
.eslintrc.js
Normal file
30
.eslintrc.js
Normal 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
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
7.2.1
|
||||
|
|
@ -1 +0,0 @@
|
|||
nodejs 20.12.2
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -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 backend’s 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 it’s 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.
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# Akkoma-FE
|
||||
# Akkoma-FE
|
||||
|
||||
 
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"target": "https://otp.akkoma.dev/",
|
||||
"target": "https://pleroma.soykaf.com/",
|
||||
"staticConfigPreference": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ var { merge } = require('webpack-merge')
|
|||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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-->
|
||||
|
|
|
|||
168
package.json
168
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/App.js
15
src/App.js
|
|
@ -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 },
|
||||
|
|
|
|||
11
src/App.scss
11
src/App.scss
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@
|
|||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<ModModal />
|
||||
<SearchFormModal />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
<script src="./about.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./avatar_list.js"></script>
|
||||
<script src="./avatar_list.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
||||
</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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
14
src/components/dm_timeline/dm_timeline.js
Normal file
14
src/components/dm_timeline/dm_timeline.js
Normal 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
|
||||
9
src/components/dm_timeline/dm_timeline.vue
Normal file
9
src/components/dm_timeline/dm_timeline.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.dms')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'dms'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./dm_timeline.js"></script>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 @, it’s 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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./features_panel.js"></script>
|
||||
<script src="./features_panel.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
.features-panel li {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./font_control.js"></script>
|
||||
<script src="./font_control.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./instance_specific_panel.js"></script>
|
||||
<script src="./instance_specific_panel.js" ></script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login_form.js"></script>
|
||||
<script src="./login_form.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</label>
|
||||
</template>
|
||||
|
||||
<script src="./media_upload.js"></script>
|
||||
<script src="./media_upload.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ const MentionLink = {
|
|||
this.highlightType
|
||||
]
|
||||
},
|
||||
useAtIcon () {
|
||||
return this.mergedConfig.useAtIcon
|
||||
},
|
||||
isRemote () {
|
||||
return this.userName !== this.userNameFull
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -69,4 +69,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./recovery_form.js"></script>
|
||||
<script src="./recovery_form.js" ></script>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
<input
|
||||
id="code"
|
||||
v-model="code"
|
||||
autocomplete="one-time-code"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
class="panel-heading"
|
||||
@click="toggleHidden"
|
||||
>
|
||||
<h4>{{ $t('moderation.reports.report') + ' ' + 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"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
class="small-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
{{ user.screen_name }}
|
||||
{{ this.user.screen_name }}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Timeago
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ export default {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'backdropClicked',
|
||||
],
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<pinch-zoom
|
||||
class="pinch-zoom-parent"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot />
|
||||
</pinch-zoom>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
{{ $t('polls.indicate_anonymous') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="poll.akkoma?.anonymous === false"
|
||||
class="alert warning"
|
||||
>
|
||||
<FAIcon icon="triangle-exclamation" />
|
||||
|
||||
{{ $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 }) }} ·
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} ·
|
||||
</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 }) }} ·
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} ·
|
||||
</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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue