Compare commits
58 Commits
froth-akko
...
froth-zero
Author | SHA1 | Date |
---|---|---|
Sam Therapy | 440c2c15b6 | |
Zero | a6a77233c8 | |
yan | cf92ca25e3 | |
yan | 23a3b1a8b3 | |
Sam Therapy | f30b28032d | |
eris | 8d391fd3d3 | |
Sam Therapy | c02e757528 | |
Zero | 53516bea57 | |
Zero | 93ea1f0659 | |
Sam Therapy | 4241b67771 | |
Sam Therapy | 61fa33739d | |
Sam Therapy | c64a627438 | |
Sam Therapy | b29f9c4e88 | |
FloatingGhost | 74b1e66dc3 | |
Zero | 99214ce111 | |
Zero | 8f99ca066a | |
Zero | 010675cb80 | |
Zero | 4176ef4996 | |
Zero | 2745aabcb4 | |
Zero | 72efb7d0e9 | |
Zero | cc09f0bcb9 | |
floatingghost | 655adfdbd8 | |
Zero | 8cff52ac32 | |
Zero | 8d3d8de7a3 | |
FloatingGhost | a6a5f03966 | |
Sol Fisher Romanoff | 83fa4ad4a7 | |
FloatingGhost | 0ab60d63ef | |
eris | 788691b296 | |
FloatingGhost | fc7cdb1e69 | |
FloatingGhost | 44047458e4 | |
Sol Fisher Romanoff | c27845169d | |
sadposter | 378ac642d4 | |
FloatingGhost | 23b60e9263 | |
FloatingGhost | e1785d2d91 | |
FloatingGhost | 3cc6a0b252 | |
FloatingGhost | 710891d1f3 | |
Zero | 6170aa3ee7 | |
Zero | afb388b73b | |
Zero | 6729ab8cf1 | |
Zero | 69671446e5 | |
Sam Therapy | d8ec9f596d | |
Zero | df8bf08f46 | |
Sam Therapy | 2d43adb6e5 | |
Zero | 402f3a61fe | |
Tusooa Zhu | 58cd2fb994 | |
Zero | e32ae82441 | |
Zero | 0f5e601db2 | |
Zero | 2f215b8d2e | |
Zero | 9c827c9834 | |
Zero | 7622b1230f | |
Ilja | 84ce0046f9 | |
Zero | 99d2c75a1a | |
Zero | fbd9ee03c4 | |
Zero | 387958c7f1 | |
Zero | 51cb9b5c8f | |
Zero | 16772d75f7 | |
Ilja | 241d520795 | |
Henry Jameson | 2ce47e8cf0 |
2
.babelrc
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
|
||||
"comments": false
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ steps:
|
|||
- name: Build
|
||||
depends_on:
|
||||
- Clone
|
||||
image: node:18
|
||||
image: node:16
|
||||
commands:
|
||||
- yarn
|
||||
- yarn build
|
||||
|
@ -39,3 +39,4 @@ steps:
|
|||
branch:
|
||||
- froth
|
||||
- froth-akkoma
|
||||
- froth-zero
|
||||
|
|
24
.eslintrc.js
|
@ -1,17 +1,18 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
sourceType: 'module'
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module',
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue',
|
||||
'import'
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
@ -21,10 +22,17 @@ module.exports = {
|
|||
'generator-star-spacing': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/component-definition-name-casing': 'off',
|
||||
'vue/component-tags-order': 'off',
|
||||
'vue/html-closing-bracket-spacing': 'off',
|
||||
'vue/html-indent': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-lone-template': 'off',
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
'vue/no-v-text-v-html-on-component': 'off',
|
||||
'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
|
||||
'vue/v-slot-style': 'off'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
name: "Bug report"
|
||||
about: "Something isn't working as expected"
|
||||
title: "[bug] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible."
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: "Which version of pleroma-fe are you running? If running develop, specify the commit hash."
|
||||
placeholder: "e.g. 2022.11, 40e86998e6"
|
||||
- type: textarea
|
||||
id: attempt
|
||||
attributes:
|
||||
label: "What were you trying to do?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reality
|
||||
attributes:
|
||||
label: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: "Severity"
|
||||
description: "Does this issue prevent you from using the software as normal?"
|
||||
options:
|
||||
- "I cannot use the software"
|
||||
- "I cannot use it as easily as I'd like"
|
||||
- "I can manage"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this issue?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this issue mentioned anywhere."
|
|
@ -1,29 +0,0 @@
|
|||
name: "Feature request"
|
||||
about: "I'd like something to be added to pleroma-fe"
|
||||
title: "[feat] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: "The idea"
|
||||
description: "What do you think you should be able to do in pleroma-fe?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: "The reasoning"
|
||||
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this feature request?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues), or the one for [the backend](https://akkoma.dev/AkkomaGang/akkoma/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this feature request mentioned anywhere."
|
||||
- label: "This feature is related to the pleroma-fe Akkoma frontend specifically, and not the backend."
|
|
@ -7,9 +7,6 @@ test/e2e/reports
|
|||
selenium-debug.log
|
||||
.idea/
|
||||
config/local.json
|
||||
config/local.*.json
|
||||
docs/site/
|
||||
.vscode/
|
||||
|
||||
.dccache
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:10
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- npm run lint
|
||||
- npm run stylelint
|
||||
|
||||
test:
|
||||
stage: test
|
||||
variables:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
script:
|
||||
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
- firefox --version
|
||||
- yarn
|
||||
- yarn unit
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- yarn
|
||||
- npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- dist/
|
||||
|
||||
docs-deploy:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
only:
|
||||
- develop@pleroma/pleroma-fe
|
||||
before_script:
|
||||
- apk add curl
|
||||
script:
|
||||
- curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' https://git.pleroma.social/api/v4/projects/673/trigger/pipeline
|
|
@ -1,9 +0,0 @@
|
|||
[submodule "instance/pleroma-mods/pleroma-mod-syntax"]
|
||||
path = instance/pleroma-mods/pleroma-mod-syntax
|
||||
url = https://git.pleroma.social/absturztaube/pleroma-mod-syntax.git
|
||||
[submodule "instance/pleroma-mods/pleroma-mod-imgsearch"]
|
||||
path = instance/pleroma-mods/pleroma-mod-imgsearch
|
||||
url = https://gitlab.com/SamTherapy/pleroma-mod-imgsearch
|
||||
[submodule "instance/pleroma-mods/pleroma-mod-math"]
|
||||
path = instance/pleroma-mods/pleroma-mod-math
|
||||
url = https://git.pleroma.social/absturztaube/pleroma-mod-math
|
|
@ -1,13 +1,19 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
true,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
82
CHANGELOG.md
|
@ -3,86 +3,6 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Implemented remote interaction with statuses
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
### Changed
|
||||
- Top bar now has navigation shortcuts. Can be enabled or disabled by admins or users.
|
||||
- Optional replacement of timeline drop-down with navigation buttons. Also configurable.
|
||||
- Posts and posts with replies are now separated on user profiles.
|
||||
- Custom emoji from remote instances on a post can now also be used.
|
||||
|
||||
## 2022.08 - 2022-08-12
|
||||
### Added
|
||||
- Ability to quote public and unlisted posts
|
||||
- Bubble timeline
|
||||
|
||||
### Changed
|
||||
- Emoji in emoji picker is separated by packs
|
||||
|
||||
### Removed
|
||||
- Chats, they were half-baked. Just use PMs.
|
||||
|
||||
## 2022.07 - 2022-07-16
|
||||
### Fixed
|
||||
- AdminFE button no longer scrolls page to top when clicked
|
||||
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
|
||||
- Fixed many many bugs related to new mentions, including spacing and alignment issues
|
||||
- Links in profile bios now properly open in new tabs
|
||||
- Inline images now respect their intended width/height attributes
|
||||
- Links with `&` in them work properly now
|
||||
- Interaction list popovers now properly emojify names
|
||||
- Completely hidden posts still had 1px border
|
||||
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
|
||||
- Attachment description is prefilled with backend-provided default when uploading
|
||||
- Proper visual feedback that next image is loading when browsing
|
||||
- Misskey-Flavoured Markdown support
|
||||
- Custom emoji reactions
|
||||
|
||||
### Changed
|
||||
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
|
||||
- User highlight background now also covers the `@`
|
||||
- Reverted back to textual `@`, svg version is opt-in.
|
||||
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
|
||||
- Uploaded attachments are uniform with displayed attachments
|
||||
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
|
||||
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
|
||||
|
||||
### Added
|
||||
- Options to show domains in mentions
|
||||
- Option to show user avatars in mention links (opt-in)
|
||||
- Option to disable the tooltip for mentions
|
||||
- Option to completely hide muted threads
|
||||
- Ability to open videos in modal even if you disabled that feature, via an icon button
|
||||
- New button on attachment that indicates that attachment has a description and shows a bar filled with description
|
||||
- Attachments are truncated just like post contents
|
||||
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||
- Ability to rearrange order of attachments when uploading
|
||||
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||
- Added frontend ui for account migration
|
||||
|
||||
|
||||
## [2.4.2] - 2022-01-09
|
||||
### Added
|
||||
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
||||
- Implemented user option to always show floating New Post button (normally mobile-only)
|
||||
- Display reasons for instance specific policies
|
||||
- Added functionality to cancel follow request
|
||||
|
||||
### Fixed
|
||||
- Fixed link to external profile not working on user profiles
|
||||
- Fixed mobile shoutbox display
|
||||
- Fixed favicon badge not working in Chrome
|
||||
- Escape html more properly in subject/display name
|
||||
|
||||
|
||||
## [2.4.0] - 2021-08-08
|
||||
### Added
|
||||
- Added a quick settings to timeline header for easier access
|
||||
|
@ -95,7 +15,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
### Fixed
|
||||
- Fixed follow request count showing in the wrong location in mobile view
|
||||
|
||||
|
||||
## [2.3.0] - 2021-03-01
|
||||
### Fixed
|
||||
- Button to remove uploaded media in post status form is now properly placed and sized.
|
||||
|
@ -108,7 +27,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
### Changed
|
||||
- Display 'people voted' instead of 'votes' for multi-choice polls
|
||||
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
|
||||
- Renamed "Timeline" to "Home Timeline" to be more clear
|
||||
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
|
||||
- When opening emoji picker or react picker, it automatically focuses the search field
|
||||
|
|
|
@ -10,5 +10,3 @@ Contributors of this project.
|
|||
- shpuld (shpuld@shitposter.club): CSS and styling
|
||||
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
|
||||
- hj (hj@shigusegubu.club): Code
|
||||
- Sean King (seanking@freespeechextremist.com): Code
|
||||
- Tusooa Zhu (tusooa@kazv.moe): Code
|
||||
|
|
31
README.md
|
@ -1,35 +1,16 @@
|
|||
# Akkoma-FE
|
||||
# Pleroma-FE
|
||||
|
||||
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
|
||||
> A single column frontend designed for Pleroma.
|
||||
|
||||
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
||||
- Custom emoji reactions
|
||||
|
||||
# Changes in this Fork
|
||||
|
||||
* script tag in index.html for [pleroma-mod-loader](https://git.pleroma.social/absturztaube/pleroma-mod-loader)
|
||||
* ability to move notifications to a seperate column
|
||||
* insert zero width space when padding of emojis is disabled
|
||||
* add custom language "English (Nyan)"
|
||||
* pointing version links to my gitlab repos
|
||||
* optional compact styles provided by craftplacer
|
||||
* tags as buttons bellow a post
|
||||
* [pinch and pan media](https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403)
|
||||
* swap of react and favorite button in status
|
||||
* adding __vueComponent__ to DOM nodes to make new mods work
|
||||
![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png)
|
||||
|
||||
# For Translators
|
||||
|
||||
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.
|
||||
|
||||
Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
|
||||
# FOR ADMINS
|
||||
|
||||
To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
||||
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
|
||||
|
||||
## Build Setup
|
||||
|
||||
|
@ -65,4 +46,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.
|
||||
|
|
|
@ -21,6 +21,7 @@ var compiler = webpack(webpackConfig)
|
|||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
writeToDisk: true,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
|
@ -28,6 +29,18 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
|||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
compiler.plugin('compilation', function (compilation) {
|
||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
// FIXME: This supposed to reload whole page when index.html is changed,
|
||||
// however now it reloads entire page on every breath, i suppose the order
|
||||
// of plugins changed or something. It's a minor thing and douesn't hurt
|
||||
// disabling it, constant reloads hurt much more
|
||||
|
||||
// hotMiddleware.publish({ action: 'reload' })
|
||||
// cb()
|
||||
})
|
||||
})
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
|
|
|
@ -2,7 +2,8 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
|
@ -18,7 +19,6 @@ module.exports = {
|
|||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
|
@ -29,19 +29,16 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||
extensions: ['.js', '.vue'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
fallback: {
|
||||
"url": require.resolve("url/"),
|
||||
},
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.runtime.common',
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
'components': path.resolve(__dirname, '../src/components')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
|
@ -61,33 +58,9 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
enforce: 'post',
|
||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
type: 'javascript/auto',
|
||||
loader: '@intlify/vue-i18n-loader',
|
||||
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: "javascript/auto"
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
isCustomElement(tag) {
|
||||
if (tag === 'pinch-zoom') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
use: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
|
@ -118,6 +91,22 @@ module.exports = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin()
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||
filename: 'sw-pleroma.js'
|
||||
}),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "node_modules/ruffle-mirror/*",
|
||||
to: "static/ruffle",
|
||||
flatten: true
|
||||
},
|
||||
],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
var config = require('../config')
|
||||
var webpack = require('webpack')
|
||||
var { merge } = require('webpack-merge')
|
||||
var merge = require('webpack-merge')
|
||||
var utils = require('./utils')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
@ -16,14 +16,12 @@ module.exports = merge(baseWebpackConfig, {
|
|||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: 'eval-source-map',
|
||||
devtool: '#eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
|
|
@ -2,8 +2,7 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
||||
var { merge } = require('webpack-merge')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
@ -20,7 +19,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
|
@ -33,18 +32,11 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
}),
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined)
|
||||
}),
|
||||
// extract css into its own file
|
||||
new MiniCssExtractPlugin({
|
||||
|
@ -68,7 +60,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'auto'
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
TARGET="pleroma@froth.zone:/opt/pleroma"
|
||||
TARGET="pleroma@froth.zone:/var/www/pleroma/zero"
|
||||
|
||||
#rsync -ra public/ "${TARGET}/instance/static"
|
||||
#cp dist/index.html "${TARGET}/instance/static/index.html"
|
||||
rsync --update --delete -Pr dist/ "${TARGET}/instance/static/frontends/pleroma-fe/froth"
|
||||
# rsync --update -ra dist/static/ "${TARGET}/instance/static/frontends/pleroma-fe/froth"
|
||||
rsync --update --delete -Pr dist/ "${TARGET}"
|
||||
#rsync --update -ra dist/static/ "${TARGET}/instance/static/static"
|
||||
#rsync --delete -ra images/ "${TARGET}/instance/static/images"
|
||||
#rsync --delete -ra sounds/ "${TARGET}/instance/static/sounds"
|
||||
rsync --update -ra instance/ "${TARGET}/instance/static/instance"
|
||||
#rsync -ra instance/ "${TARGET}/instance/static/instance"
|
||||
#rsync --delete -ra pages/ "${TARGET}/instance/static/pages"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var { merge } = require('webpack-merge')
|
||||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"target": "https://ihatebeinga.live",
|
||||
"staticConfigPreference": false
|
||||
}
|
|
@ -38,11 +38,6 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
|
@ -57,26 +52,13 @@ module.exports = {
|
|||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
'Origin': target
|
||||
}
|
||||
ws: true
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/static/stickers.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/static/stickers': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
}
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var { merge } = require('webpack-merge')
|
||||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/modules/instance.js) )
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
||||
|
||||
## Instance-defaults
|
||||
|
||||
|
@ -70,6 +70,9 @@ Default post formatting option (markdown/bbcode/plaintext/etc...)
|
|||
### `redirectRootNoLogin`, `redirectRootLogin`
|
||||
These two settings should point to where FE should redirect visitor when they login/open up website root
|
||||
|
||||
### `scopeCopy`
|
||||
Copy post scope (visibility) when replying to a post. Instance-default.
|
||||
|
||||
### `sidebarRight`
|
||||
Change alignment of sidebar and panels to the right. Defaults to `false`.
|
||||
|
|
@ -25,7 +25,7 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build*
|
|||
|
||||
### Replacing your instance's frontend with custom FE build
|
||||
|
||||
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](https://docs.akkoma.dev/stable/configuration/static_dir/), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
||||
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
||||
|
||||
### Running production build locally or on a separate server
|
||||
|
||||
|
@ -67,7 +67,7 @@ server {
|
|||
|
||||
### API, Data, Operations
|
||||
|
||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
all: install
|
||||
pipenv run mkdocs build
|
||||
|
||||
install:
|
||||
pipenv install
|
||||
clean:
|
||||
rm -rf site
|
10
docs/Pipfile
|
@ -1,10 +0,0 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
mkdocs-material = "*"
|
||||
markdown-include = "*"
|
|
@ -1,277 +0,0 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "926d34630c729228bb015cb958c04f8269c57f5ca1ffc2ceab1dfd1798884772"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
|
||||
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.3"
|
||||
},
|
||||
"ghp-import": {
|
||||
"hashes": [
|
||||
"sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619",
|
||||
"sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"
|
||||
],
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670",
|
||||
"sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.12.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.2"
|
||||
},
|
||||
"markdown": {
|
||||
"hashes": [
|
||||
"sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874",
|
||||
"sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.3.7"
|
||||
},
|
||||
"markdown-include": {
|
||||
"hashes": [
|
||||
"sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
|
||||
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
|
||||
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
|
||||
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
|
||||
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
|
||||
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
|
||||
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
|
||||
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
|
||||
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
|
||||
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
|
||||
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
|
||||
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
|
||||
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
|
||||
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
|
||||
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
|
||||
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
|
||||
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
|
||||
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
|
||||
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
|
||||
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
|
||||
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
|
||||
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
|
||||
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
|
||||
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
|
||||
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
|
||||
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
|
||||
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
|
||||
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
|
||||
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
|
||||
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
|
||||
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
|
||||
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
|
||||
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
|
||||
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
|
||||
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
|
||||
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
|
||||
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
|
||||
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
|
||||
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
|
||||
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"mergedeep": {
|
||||
"hashes": [
|
||||
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
|
||||
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"mkdocs": {
|
||||
"hashes": [
|
||||
"sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde",
|
||||
"sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:263f2721f3abe533b61f7c8bed435a0462620912742c919821ac2d698b4bfe67",
|
||||
"sha256:dc82b667d2a83f0de581b46a6d0949732ab77e7638b87ea35b770b33bc02e75a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.3.9"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
"sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44",
|
||||
"sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb",
|
||||
"sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.12.0"
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0",
|
||||
"sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==9.5"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
|
||||
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.8'",
|
||||
"version": "==3.0.9"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.2"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
|
||||
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
|
||||
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
|
||||
"sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
|
||||
"sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
|
||||
"sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
|
||||
"sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
|
||||
"sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
|
||||
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
|
||||
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
|
||||
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
|
||||
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
|
||||
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
|
||||
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
|
||||
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
|
||||
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
|
||||
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
|
||||
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
|
||||
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
|
||||
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
|
||||
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
|
||||
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
|
||||
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
|
||||
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
|
||||
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
|
||||
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
|
||||
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
|
||||
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
|
||||
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
|
||||
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
|
||||
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
|
||||
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"pyyaml-env-tag": {
|
||||
"hashes": [
|
||||
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
|
||||
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
"sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412",
|
||||
"sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654",
|
||||
"sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306",
|
||||
"sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33",
|
||||
"sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd",
|
||||
"sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7",
|
||||
"sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892",
|
||||
"sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609",
|
||||
"sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6",
|
||||
"sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1",
|
||||
"sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591",
|
||||
"sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d",
|
||||
"sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d",
|
||||
"sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c",
|
||||
"sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3",
|
||||
"sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39",
|
||||
"sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213",
|
||||
"sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330",
|
||||
"sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428",
|
||||
"sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1",
|
||||
"sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846",
|
||||
"sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153",
|
||||
"sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3",
|
||||
"sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9",
|
||||
"sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.1.9"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
|
||||
"sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.8.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 491 B |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
@ -1,28 +0,0 @@
|
|||
p, a, li, pre {
|
||||
font-family: "Tiresias PCFont", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important;
|
||||
}
|
||||
|
||||
code,
|
||||
.codehilite pre {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:root > * {
|
||||
--md-primary-fg-color: #593196;
|
||||
--md-accent-fg-color: #455a63;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Tiresias PCFont';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Tiresias PCFont'), local('Tiresias PCFont'),
|
||||
url('./fonts/Tiresias_PCfont.ttf') format('truetype')
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Tiresias Infofont';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Tiresias Infofont'), local('Tiresias Infofont'),
|
||||
url('./fonts/Tiresias_Infofont.ttf') format('truetype')
|
||||
}
|
Before Width: | Height: | Size: 530 B |
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg4485"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
sodipodi:docname="pleroma_logo_vector_nobg.svg"
|
||||
inkscape:version="0.92.1 r15371">
|
||||
<metadata
|
||||
id="metadata4491">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4489" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="997"
|
||||
id="namedview4487"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.70710678"
|
||||
inkscape:cx="26.131594"
|
||||
inkscape:cy="235.37499"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="22"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4485" />
|
||||
<g
|
||||
id="g4612">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4495"
|
||||
d="M 235,89 V 423 H 152 V 115 l 26,-26 z"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="178"
|
||||
cy="115"
|
||||
id="path4497"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="335"
|
||||
cy="230"
|
||||
id="path4497-0"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4516"
|
||||
d="M 277,256 V 89 l 84,3e-6 L 361.00002,230 335,256 Z"
|
||||
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="26"
|
||||
cx="335"
|
||||
cy="397"
|
||||
id="path4497-0-6"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4516-5"
|
||||
d="m 277,423 v -83 h 84 l 2e-5,57 L 335,423 Z"
|
||||
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
|
@ -1,53 +0,0 @@
|
|||
# Adding stickers
|
||||
|
||||
Pleroma-fe supports stickers, which are essentially little images stored server-side
|
||||
which can be selected by a user to automatically attach them to a post.
|
||||
|
||||
There's no explicit setting for these, they just rely on the existence of certain files.
|
||||
|
||||
## Initialising the sticker config file
|
||||
|
||||
You're probably serving pleroma-fe from your instance's `instance/static/` directory -
|
||||
this directy can also override files served at a given path.
|
||||
|
||||
The first thing we need to do is set up our `stickers.json` file. At `instance/static/static/stickers.json`,
|
||||
put a file that looks like this
|
||||
|
||||
```json
|
||||
{
|
||||
"myPack": "/static/stickers/myPack"
|
||||
}
|
||||
```
|
||||
|
||||
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
|
||||
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
|
||||
|
||||
## Creating the pack
|
||||
|
||||
First, create your pack directory
|
||||
|
||||
```bash
|
||||
mkdir -p instance/static/static/stickers/myPack
|
||||
```
|
||||
|
||||
Now you need to give it some config.
|
||||
|
||||
At `instance/static/static/stickers/myPack/pack.json`, put a file that looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "myPack",
|
||||
"author": "me for i am very cool",
|
||||
"tabIcon": "tab.png",
|
||||
"stickers": [
|
||||
"mySticker.png"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This should be relatively self-explanatory, it declares a pack with a title `myPack` which has only one sticker in it.
|
||||
The `tabIcon` will appear on the sticker picker itself as a representative of the pack.
|
||||
|
||||
You can add as many stickers as you like. They should all be in the same directory as your `pack.json`.
|
||||
|
||||
Now you should find that there's a sticky note icon on the emoji picker on pleroma-fe that allows you to attach stickers.
|
|
@ -1,37 +0,0 @@
|
|||
site_name: Pleroma-FE Documentation
|
||||
theme:
|
||||
favicon: 'images/pleroma_logo_vector_bg_32.png'
|
||||
name: 'material'
|
||||
custom_dir: 'theme'
|
||||
# Disable google fonts
|
||||
font: false
|
||||
logo: 'images/pleroma_logo_vector_nobg.svg'
|
||||
features:
|
||||
- tabs
|
||||
palette:
|
||||
primary: 'deep purple'
|
||||
accent: 'blue grey'
|
||||
|
||||
extra_css:
|
||||
- css/extra.css
|
||||
repo_name: 'AkkomaGang/pleroma-fe'
|
||||
repo_url: 'https://akkoma.dev/AkkomaGang/pleroma-fe'
|
||||
|
||||
extra:
|
||||
repo_icon: gitea
|
||||
|
||||
markdown_extensions:
|
||||
# Note/warning blocks https://squidfunk.github.io/mkdocs-material/extensions/admonition/
|
||||
- admonition
|
||||
- codehilite:
|
||||
guess_lang: false
|
||||
# Make it possible to link to every header https://squidfunk.github.io/mkdocs-material/extensions/permalinks/
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed
|
||||
- pymdownx.details
|
||||
- markdown_include.include:
|
||||
base_path: docs
|
|
@ -1,22 +0,0 @@
|
|||
click==8.1.3
|
||||
ghp-import==2.1.0
|
||||
importlib-metadata==4.12.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.3.7
|
||||
markdown-include==0.6.0
|
||||
MarkupSafe==2.1.1
|
||||
mergedeep==1.3.4
|
||||
mkdocs==1.3.0
|
||||
mkdocs-bootswatch==1.1
|
||||
mkdocs-material==8.1.8
|
||||
mkdocs-material-extensions==1.0.3
|
||||
packaging==21.3
|
||||
Pygments==2.11.2
|
||||
pymdown-extensions==9.1
|
||||
pyparsing==3.0.9
|
||||
python-dateutil==2.8.2
|
||||
PyYAML==6.0
|
||||
pyyaml_env_tag==0.1
|
||||
six==1.16.0
|
||||
watchdog==2.1.9
|
||||
zipp==3.8.0
|
|
@ -1,64 +0,0 @@
|
|||
<!--
|
||||
Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
{% import "partials/language.html" as lang with context %}
|
||||
|
||||
<!--
|
||||
Check whether the repository is hosted on one of the supported code hosting
|
||||
platforms (GitHub, GitLab or Bitbucket) to show icon.
|
||||
-->
|
||||
{% set platform = config.extra.repo_icon or config.repo_url %}
|
||||
{% if "github" in platform %}
|
||||
{% set repo_type = "github" %}
|
||||
{% elif "gitlab" in platform %}
|
||||
{% set repo_type = "gitlab" %}
|
||||
{% elif "bitbucket" in platform %}
|
||||
{% set repo_type = "bitbucket" %}
|
||||
{% else %}
|
||||
{% set repo_type = "" %}
|
||||
{% endif %}
|
||||
|
||||
{% if page and page.url.startswith('backend') %}
|
||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
|
||||
{% set repo_name = "pleroma/pleroma" %}
|
||||
{% elif page and page.url.startswith('frontend') %}
|
||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
|
||||
{% set repo_name = "pleroma/pleroma-fe" %}
|
||||
{% else %}
|
||||
{% set repo_url = config.repo_url %}
|
||||
{% set repo_name = config.repo_name %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Repository containing source -->
|
||||
<a href="{{ repo_url }}" title="{{ lang.t('source.link.title') }}"
|
||||
class="md-source" data-md-source="{{ repo_type }}">
|
||||
{% if repo_type %}
|
||||
<div class="md-source__icon">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<use xlink:href="#__{{ repo_type }}" width="24" height="24"></use>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="md-source__repository">
|
||||
{{ repo_name }}
|
||||
</div>
|
||||
</a>
|
|
@ -65,16 +65,6 @@ If you set the input-method to Markdown, and post this, it will look something l
|
|||
|
||||
![example_markdown](../assets/example_markdown.png)
|
||||
|
||||
## Misskey markdown
|
||||
|
||||
The akkoma version of pleroma-fe includes support for writing and rendering
|
||||
misskey markdown (MFM). To write this you will need to select "MFM" from
|
||||
the content type dropdown (if supported), and then you can format text
|
||||
[in MFM](https://akkoma.dev/sfr/marked-mfm/src/branch/master/docs/syntax.md).
|
||||
|
||||
To render MFM, you will need to turn on "Render Misskey Markdown" in
|
||||
the settings modal.
|
||||
|
||||
## Other actions
|
||||
|
||||
In addition to posting you can also *favorite* posts also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies the post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post".
|
|
@ -6,7 +6,6 @@ You have several timelines to browse trough
|
|||
- **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark.
|
||||
- **Direct Messages** all posts with `direct` scope addressed to you or mentioning you.
|
||||
- **Public Timelines** all public posts made by users on the instance you're on
|
||||
- **Bubble Timeline** all public posts from instances recommended by your admin(s) in the instance settings. This won't appear if they haven't set anything up for it.
|
||||
- **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there.
|
||||
|
||||
Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering).
|
10
index.html
|
@ -3,21 +3,13 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<title>The Froth Zone</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-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body class="hidden">
|
||||
<noscript>To use The Froth Zone, please enable JavaScript.</noscript>
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<!--<script type="text/javascript" src="/instance/pleroma-mod-loader.js"></script>-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
Before Width: | Height: | Size: 1.2 MiB |
|
@ -1,39 +0,0 @@
|
|||
<div style="margin-left: 12px; margin-right: 12px">
|
||||
<p style="text-align: center">
|
||||
Welcome to the Froth Zone! | <a rel="noopener noreferrer nofollow" href="/akkoma/swaggerui/">API Documentation</a>
|
||||
<br />
|
||||
<br />
|
||||
Available on
|
||||
<a href="https://froth.oz" rel="noopener noreferrer nofollow">OpenNIC</a>
|
||||
and
|
||||
<a href="http://froth5px5mpxckho357mtx62v5dmttrlp7prhs7nwppmmi5napuouiad.onion" rel="noopener noreferrer nofollow">Tor</a>
|
||||
(Functionality not guaranteed)
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://bloat.froth.zone" rel="noopener noreferrer nofollow">Bloat FE</a>
|
||||
|
|
||||
<a href="/web" rel="noopener noreferrer nofollow">Masto FE</a>
|
||||
|
|
||||
<a href="/main" rel="noopener noreferrer nofollow">Akkoma FE</a>
|
||||
|
|
||||
<a href="https://pl.froth.zone" rel="noopener noreferrer nofollow">Pleroma FE</a>
|
||||
|
|
||||
<a href="https://z.pl.froth.zone" rel="noopener noreferrer nofollow">Old FE</a>
|
||||
|
|
||||
<a href="https://soap.froth.zone" rel="noopener noreferrer nofollow">Soapbox FE</a>
|
||||
|
|
||||
<a href="https://treebird.froth.zone" rel="noopener noreferrer nofollow">Treebird FE</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://git.froth.zone" rel="noopener noreferrer nofollow">Gitea</a>
|
||||
|
|
||||
<a href="https://funkwhale.samtherapy.net" rel="noopener noreferrer nofollow">Funkwhale</a>
|
||||
|
|
||||
<a href="https://mk.froth.zone" rel="noopener noreferrer nofollow">Misskey</a>
|
||||
|
|
||||
<a href="https://tube.froth.zone" rel="noopener noreferrer nofollow">PeerTube</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://status.froth.zone/status" rel="noopener noreferrer nofollow">Everything else I host</a>
|
||||
</p>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"modDirectory": "/instance/pleroma-mods/",
|
||||
"mods": [
|
||||
"math",
|
||||
"syntax"
|
||||
]
|
||||
}
|
|
@ -1,477 +0,0 @@
|
|||
class PleromaModLoader {
|
||||
constructor () {
|
||||
this.config = {
|
||||
'modDirectory': '/instance/pleroma-mods/',
|
||||
'mods': []
|
||||
}
|
||||
this.loadConfig()
|
||||
this.loadedMods = {}
|
||||
this.classes = {}
|
||||
}
|
||||
|
||||
loadMods () {
|
||||
this.config.mods.forEach((mod) => {
|
||||
this.loadMod(mod)
|
||||
})
|
||||
}
|
||||
|
||||
loadMod (modName) {
|
||||
return PleromaModLoader.includeScript(
|
||||
this.config.modDirectory + 'pleroma-mod-' + modName + '/mod.js'
|
||||
).then(() => {
|
||||
this.loadedMods[modName] = new this.classes[PleromaModLoader.getClassName(modName)]()
|
||||
})
|
||||
}
|
||||
|
||||
loadConfig () {
|
||||
window.fetch('/instance/pleroma-mod-config.json').then((response) => {
|
||||
return response.json()
|
||||
}).then((json) => {
|
||||
Object.keys(json).forEach((key) => {
|
||||
this.config[key] = json[key]
|
||||
})
|
||||
this.loadMods()
|
||||
}).catch((error) => {
|
||||
console.error('can\'t load loader config')
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
registerClass (className, object) {
|
||||
this.classes[className] = object
|
||||
}
|
||||
|
||||
waitUntilReady () {
|
||||
const postPanel = document.querySelector('.status-container')
|
||||
const loginPanel = document.querySelector('.login-form')
|
||||
if (postPanel || loginPanel) {
|
||||
Object.keys(this.loadedMods).forEach((modName) => {
|
||||
const settings = document.querySelector('.settings-modal div[label]:first-child')
|
||||
if (settings) {
|
||||
if (!settings.querySelector('.mod-settings')) {
|
||||
this.appendModSettings(settings)
|
||||
}
|
||||
}
|
||||
|
||||
const mod = this.loadedMods[modName]
|
||||
if (mod.isEnabled) {
|
||||
mod.ready()
|
||||
}
|
||||
})
|
||||
|
||||
this.createObserver()
|
||||
} else {
|
||||
console.warn('not ready, trying again in 1s')
|
||||
window.setTimeout(() => { this.waitUntilReady() }, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
createCheckbox (label, mod) {
|
||||
const labelElement = document.createElement('label')
|
||||
labelElement.classList.add('checkbox')
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.setAttribute('type', 'checkbox')
|
||||
input.checked = mod.isEnabled
|
||||
input.addEventListener('change', (event) => {
|
||||
if (event.target.checked) {
|
||||
mod.enable()
|
||||
} else {
|
||||
mod.disable()
|
||||
}
|
||||
})
|
||||
labelElement.appendChild(input)
|
||||
|
||||
const fakeCheckbox = document.createElement('i')
|
||||
fakeCheckbox.classList.add('checkbox-indicator')
|
||||
labelElement.appendChild(fakeCheckbox)
|
||||
|
||||
const text = document.createElement('span')
|
||||
text.classList.add('label')
|
||||
text.innerText = label
|
||||
labelElement.appendChild(text)
|
||||
|
||||
return labelElement
|
||||
}
|
||||
|
||||
appendModSettings (element) {
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('setting-item')
|
||||
container.classList.add('mod-settings')
|
||||
|
||||
const title = document.createElement('h2')
|
||||
title.innerText = 'Pleroma Mods'
|
||||
container.appendChild(title)
|
||||
|
||||
const optionList = document.createElement('ul')
|
||||
optionList.classList.add('setting-list')
|
||||
|
||||
Object.keys(this.loadedMods).sort().forEach((modName) => {
|
||||
const li = document.createElement('li')
|
||||
|
||||
const enable = this.createCheckbox('enable ' + modName, this.loadedMods[modName])
|
||||
li.appendChild(enable)
|
||||
|
||||
const ulConfig = document.createElement('ul')
|
||||
ulConfig.classList.add('setting-list')
|
||||
|
||||
Object.keys(this.loadedMods[modName].config).forEach((key) => {
|
||||
if (key === 'includes' || key === 'filter') {
|
||||
return
|
||||
}
|
||||
|
||||
this.loadedMods[modName].onSettingInit(key, ulConfig, document.createElement('li'))
|
||||
})
|
||||
|
||||
li.appendChild(ulConfig)
|
||||
|
||||
optionList.appendChild(li)
|
||||
})
|
||||
|
||||
container.appendChild(optionList)
|
||||
|
||||
element.appendChild(container)
|
||||
}
|
||||
|
||||
createObserver () {
|
||||
this.containers = {
|
||||
main: document.querySelector('.main'),
|
||||
notifications: document.querySelector('.notifications'),
|
||||
userPanel: document.querySelector('.user-panel'),
|
||||
settingsModal: document.querySelector('.settings-modal')
|
||||
}
|
||||
|
||||
const observerConfig = {
|
||||
subtree: true,
|
||||
childList: true
|
||||
}
|
||||
|
||||
this.observer = new MutationObserver((mutations, observer) => {
|
||||
const modal = document.querySelector('.settings-modal div[label]:first-child')
|
||||
if (modal && !modal.querySelector('.mod-settings')) {
|
||||
this.appendModSettings(modal)
|
||||
}
|
||||
|
||||
Object.values(this.loadedMods).forEach((mod) => {
|
||||
if (mod.isEnabled) {
|
||||
mutations.forEach((mutation) => {
|
||||
mod.mutate(mutation, observer)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.observer.observe(this.containers.main, observerConfig)
|
||||
if (this.containers.notifications) {
|
||||
this.observer.observe(this.containers.notifications, observerConfig)
|
||||
}
|
||||
if (this.containers.userPanel) {
|
||||
this.observer.observe(this.containers.userPanel, observerConfig)
|
||||
}
|
||||
if (this.containers.settingsModal) {
|
||||
this.observer.observe(this.containers.settingsModal, observerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
static registerMod (mod) {
|
||||
window.__pleromaModLoader.registerClass(mod.name, mod)
|
||||
}
|
||||
|
||||
static includeScript (src) {
|
||||
console.log('include ' + src)
|
||||
return new Promise((resolve) => {
|
||||
const script = document.createElement('script')
|
||||
script.setAttribute('src', src)
|
||||
script.setAttribute('type', 'text/javascript')
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
document.querySelector('body').appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
static includeCss (src) {
|
||||
console.log('include ' + src)
|
||||
return new Promise((resolve) => {
|
||||
const link = document.createElement('link')
|
||||
link.setAttribute('href', src)
|
||||
link.setAttribute('rel', 'stylesheet')
|
||||
link.setAttribute('type', 'text/css')
|
||||
link.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
document.querySelector('head').appendChild(link)
|
||||
})
|
||||
}
|
||||
|
||||
static excludeScript (src) {
|
||||
return new Promise((resolve) => {
|
||||
const script = document.querySelector('script[src=\'' + src + '\']')
|
||||
if (script) {
|
||||
script.remove()
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
static excludeCss (src) {
|
||||
return new Promise((resolve) => {
|
||||
const link = document.querySelector('link[href=\'' + src + '\']')
|
||||
if (link) {
|
||||
link.remove()
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
static getVueScope (element) {
|
||||
if (!element) {
|
||||
return null
|
||||
}
|
||||
if (element.__vue__) {
|
||||
console.warn('old vue version, please update pleroma-fe')
|
||||
return element.__vue__
|
||||
}
|
||||
if (element._vnode) {
|
||||
return element._vnode
|
||||
}
|
||||
if (element.__vnode) {
|
||||
return element.__vnode
|
||||
}
|
||||
if (element.parentNode) {
|
||||
return PleromaModLoader.getVueScope(element.parentNode)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static getVueComponent (element) {
|
||||
if (!element) {
|
||||
return null
|
||||
}
|
||||
if (element.__vnode && element.__vnode.component) {
|
||||
return element.__vnode.component
|
||||
}
|
||||
if (element.__vueParentComponent) {
|
||||
return element.__vueParentComponent.ctx
|
||||
}
|
||||
if (element.__vueComponent__) {
|
||||
return element.__vueComponent__
|
||||
}
|
||||
if (element.parentNode) {
|
||||
return PleromaModLoader.getVueComponent(element.parentNode)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static getRootVueScope () {
|
||||
return PleromaModLoader.getVueScope(document.querySelector('#app'))
|
||||
}
|
||||
|
||||
static getToken () {
|
||||
return PleromaModLoader.getRootVueScope().appContext.provides.store.getters.getUserToken()
|
||||
}
|
||||
|
||||
static getModDir () {
|
||||
return window.__pleromaModLoader.config.modDirectory
|
||||
}
|
||||
|
||||
static getClassName (name) {
|
||||
let className = 'PleromaMod'
|
||||
name.split('-').forEach((namePart) => {
|
||||
className += namePart.substring(0, 1).toUpperCase()
|
||||
className += namePart.substring(1)
|
||||
})
|
||||
return className
|
||||
}
|
||||
|
||||
static api (method, path, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = PleromaModLoader.getToken()
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.responseType = 'json'
|
||||
xhr.open(method, path)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token)
|
||||
xhr.onreadstatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status !== 200) {
|
||||
reject(new Error({
|
||||
status: xhr.status,
|
||||
response: xhr.response,
|
||||
xhr: xhr
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
xhr.send(JSON.stringify(params))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PleromaMod { // eslint-disable-line no-unused-vars
|
||||
constructor (name) {
|
||||
this.name = name
|
||||
this.config = {}
|
||||
this.isRunning = false
|
||||
}
|
||||
|
||||
get isEnabled () {
|
||||
return localStorage.getItem('pleroma_mod_' + this.name + '_enabled') !== 'false' || true
|
||||
}
|
||||
|
||||
getClassName () {
|
||||
return PleromaModLoader.getClassName(this.name)
|
||||
}
|
||||
|
||||
getModDir () {
|
||||
return PleromaModLoader.getModDir() + this.name + '/'
|
||||
}
|
||||
|
||||
ready () {
|
||||
this.onReady()
|
||||
this.run()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.isRunning = false
|
||||
if (this.config.includes) {
|
||||
const styles = this.config.includes.css || []
|
||||
const scripts = this.config.includes.js || []
|
||||
|
||||
Promise.all(styles.map((style) => {
|
||||
return this.excludeCss(style)
|
||||
}).concat(scripts.map((script) => {
|
||||
return this.excludeScript(script)
|
||||
}))).then(() => {
|
||||
this.onDestroy()
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.onDestroy()
|
||||
}
|
||||
|
||||
run () {
|
||||
if (this.config.includes) {
|
||||
const styles = this.config.includes.css || []
|
||||
const scripts = this.config.includes.js || []
|
||||
|
||||
Promise.all(styles.map((style) => {
|
||||
return this.includeCss(style)
|
||||
}).concat(scripts.map((script) => {
|
||||
return this.includeScript(script)
|
||||
})).concat([
|
||||
this.loadConfig(),
|
||||
this.onCreate()
|
||||
])).then(() => {
|
||||
this.isRunning = true
|
||||
this.onRun()
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning = true
|
||||
this.onRun()
|
||||
}
|
||||
|
||||
mutate (mutation, observer) {
|
||||
if (this.isRunning) {
|
||||
this.onMutation(mutation, observer)
|
||||
}
|
||||
}
|
||||
|
||||
saveConfig () {
|
||||
const storedConfig = {}
|
||||
Object.keys(this.config).filter((key) => {
|
||||
return key !== 'includes' && key !== 'filter'
|
||||
}).forEach((key) => {
|
||||
storedConfig[key] = this.config[key]
|
||||
})
|
||||
localStorage.setItem(this.name + '_config', JSON.stringify(storedConfig))
|
||||
}
|
||||
|
||||
mergeConfig (newConfig) {
|
||||
Object.keys(newConfig).forEach((key) => {
|
||||
this.config[key] = JSON.parse(JSON.stringify(newConfig[key]))
|
||||
})
|
||||
}
|
||||
|
||||
loadConfig () {
|
||||
return new Promise((resolve) => {
|
||||
// TODO: use structuredClone when its more supported
|
||||
this.defaultConfig = JSON.parse(JSON.stringify(this.config))
|
||||
|
||||
const storedConfig = JSON.parse(localStorage.getItem(this.name + '_config'))
|
||||
|
||||
this.onConfigLoad().then((json) => {
|
||||
this.mergeConfig(json)
|
||||
if (storedConfig) {
|
||||
this.mergeConfig(storedConfig)
|
||||
}
|
||||
this.saveConfig()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onReady () {}
|
||||
onCreate () {
|
||||
return new Promise((resolve) => {
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
onDestroy () {}
|
||||
onRun () {}
|
||||
onMutation (mutation, observer) {}
|
||||
onConfigLoad () {
|
||||
return new Promise((resolve) => {
|
||||
resolve({})
|
||||
})
|
||||
}
|
||||
onSettingInit (key, ul, li) {}
|
||||
|
||||
includeCss (src) {
|
||||
return PleromaModLoader.includeCss(PleromaModLoader.getModDir() + this.name + '/' + src)
|
||||
}
|
||||
|
||||
includeScript (src) {
|
||||
return PleromaModLoader.includeScript(PleromaModLoader.getModDir() + this.name + '/' + src)
|
||||
}
|
||||
|
||||
excludeCss (src) {
|
||||
return PleromaModLoader.excludeCss(PleromaModLoader.getModDir() + this.name + '/' + src)
|
||||
}
|
||||
|
||||
excludeScript (src) {
|
||||
return PleromaModLoader.excludeScript(PleromaModLoader.getModDir() + this.name + '/' + src)
|
||||
}
|
||||
|
||||
fetchJson (src) {
|
||||
console.log('loading ' + src)
|
||||
return window.fetch(PleromaModLoader.getModDir() + this.name + '/' + src).then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
}
|
||||
|
||||
api (method, path, params) {
|
||||
return PleromaModLoader.api(method, path, params)
|
||||
}
|
||||
|
||||
enable () {
|
||||
this.ready()
|
||||
localStorage.setItem('pleroma_mod_' + this.name + '_enabled', true)
|
||||
}
|
||||
|
||||
disable () {
|
||||
this.destroy()
|
||||
localStorage.setItem('pleroma_mod_' + this.name + '_enabled', false)
|
||||
}
|
||||
}
|
||||
|
||||
window.__pleromaModLoader = new PleromaModLoader()
|
||||
window.__pleromaModLoader.waitUntilReady()
|
|
@ -1 +0,0 @@
|
|||
Subproject commit cfd4dd6404d2df6e07e646aff5ff1d606ed48228
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 5d7ac570770bd7f1d5cba1279c444b53626add3c
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 440dd5dcb4c97d58726a2a52abbe9c54a8b2f3fb
|
208
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.5.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"version": "1.0.0",
|
||||
"description": "A Qvitter-style frontend for certain GS servers.",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -11,124 +11,118 @@
|
|||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "stylelint src/**/*.scss",
|
||||
"stylelint": "npx stylelint src/components/status/status.scss",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"blurhash": "^2.0.4",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"js-cookie": "^3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
"qrcode": "1",
|
||||
"url": "^0.11.0",
|
||||
"vue": "^3.2.31",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "4.0.14",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuex": "4.0.2"
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.0",
|
||||
"body-scroll-lock": "^2.6.4",
|
||||
"chromatism": "^3.0.0",
|
||||
"cropperjs": "^1.4.3",
|
||||
"diff": "^3.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"localforage": "^1.5.0",
|
||||
"parse-link-header": "^1.0.1",
|
||||
"phoenix": "^1.3.0",
|
||||
"portal-vue": "^2.1.4",
|
||||
"punycode.js": "^2.1.0",
|
||||
"ruffle-mirror": "^2021.4.10",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.7.14",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@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.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": "6.7.7",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "^107.0.3",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"css-loader": "^6.7.2",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@ungap/event-target": "^0.1.0",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
|
||||
"@vue/babel-preset-jsx": "^1.2.4",
|
||||
"@vue/test-utils": "^1.0.0-beta.26",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"chai": "^3.5.0",
|
||||
"chalk": "^1.1.3",
|
||||
"chromedriver": "^87.0.1",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-spawn": "^4.0.2",
|
||||
"css-loader": "^0.28.0",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.17.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"isparta-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.3.17",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-firefox-launcher": "1.3.0",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lolex": "1.6.0",
|
||||
"mini-css-extract-plugin": "0.12.0",
|
||||
"mocha": "3.5.3",
|
||||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"eslint-plugin-import": "^2.13.0",
|
||||
"eslint-plugin-node": "^7.0.0",
|
||||
"eslint-plugin-promise": "^4.0.0",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.13.3",
|
||||
"file-loader": "^3.0.1",
|
||||
"function-bind": "^1.0.2",
|
||||
"html-webpack-plugin": "^3.0.0",
|
||||
"http-proxy-middleware": "^0.17.2",
|
||||
"inject-loader": "^2.0.1",
|
||||
"iso-639-1": "^2.0.3",
|
||||
"isparta-loader": "^2.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^3.0.0",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-mocha": "^1.2.0",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-sinon-chai": "^2.0.2",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.26",
|
||||
"karma-webpack": "^4.0.0-rc.3",
|
||||
"lodash": "^4.16.4",
|
||||
"lolex": "^1.4.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"mocha": "^3.1.0",
|
||||
"nightwatch": "^0.9.8",
|
||||
"opn": "^4.0.2",
|
||||
"ora": "^0.3.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass": "^1.17.3",
|
||||
"sass-loader": "^10",
|
||||
"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",
|
||||
"semver": "^5.3.0",
|
||||
"serviceworker-webpack-plugin": "^1.0.0",
|
||||
"shelljs": "^0.8.4",
|
||||
"sinon": "^2.1.0",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"stylelint": "^13.6.1",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-rscss": "^0.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-dev-middleware": "^5.3.3",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
"url-loader": "^1.1.2",
|
||||
"vue-loader": "^14.0.0",
|
||||
"vue-style-loader": "^4.0.0",
|
||||
"webpack": "^4.44.0",
|
||||
"webpack-dev-middleware": "^3.6.0",
|
||||
"webpack-hot-middleware": "^2.12.2",
|
||||
"webpack-merge": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0",
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.20.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
68
src/App.js
|
@ -1,31 +1,28 @@
|
|||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import Notifications from './components/notifications/notifications.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
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 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'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
Notifications,
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -36,11 +33,8 @@ export default {
|
|||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
ModModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -52,20 +46,10 @@ export default {
|
|||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
unmounted () {
|
||||
destroyed () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
},
|
||||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
|
@ -81,46 +65,48 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
shout () { return this.$store.state.shout.joined },
|
||||
shout () { return this.$store.state.shout.channel.state === 'joined' },
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
thirdColumnEnabled () {
|
||||
return this.$store.getters.mergedConfig.showThirdColumn || false
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
},
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
sidebarAlign () {
|
||||
return {
|
||||
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
|
||||
}
|
||||
},
|
||||
notifsAlign () {
|
||||
return {
|
||||
'order': this.$store.getters.mergedConfig.sidebarRight ? 0 : 99
|
||||
}
|
||||
},
|
||||
thirdColumnLayout () {
|
||||
return {
|
||||
'max-width': this.$store.getters.mergedConfig.showThirdColumn ? '1400px' : '980px'
|
||||
}
|
||||
},
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
updateMobileState () {
|
||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
const layoutHeight = windowHeight()
|
||||
const changed = mobileLayout !== this.isMobileLayout
|
||||
if (changed) {
|
||||
this.$store.dispatch('setMobileLayout', mobileLayout)
|
||||
}
|
||||
this.$store.dispatch('setLayoutHeight', layoutHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
860
src/App.scss
69
src/App.vue
|
@ -1,39 +1,41 @@
|
|||
<template>
|
||||
<div
|
||||
id="app-loaded"
|
||||
id="app"
|
||||
:style="bgStyle"
|
||||
>
|
||||
<div
|
||||
id="app_bg_wrapper"
|
||||
class="app-bg-wrapper"
|
||||
/>
|
||||
<MobileNav v-if="layoutType === 'mobile'" />
|
||||
<MobileNav v-if="isMobileLayout" />
|
||||
<DesktopNav v-else />
|
||||
<Notifications v-if="currentUser" />
|
||||
<div class="app-bg-wrapper app-container-wrapper" />
|
||||
<div
|
||||
id="content"
|
||||
class="app-layout container"
|
||||
:class="classes"
|
||||
:style="thirdColumnLayout"
|
||||
class="container underlay"
|
||||
>
|
||||
<div class="underlay" />
|
||||
<div
|
||||
id="sidebar"
|
||||
class="column -scrollable"
|
||||
:class="{ '-show-scrollbar': showScrollbars }"
|
||||
class="sidebar-flexer mobile-hidden"
|
||||
:style="sidebarAlign"
|
||||
>
|
||||
<user-panel />
|
||||
<template v-if="layoutType !== 'mobile'">
|
||||
<nav-panel />
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
||||
<div id="notifs-sidebar" />
|
||||
</template>
|
||||
<div class="sidebar-bounds">
|
||||
<div class="sidebar-scroller">
|
||||
<div class="sidebar">
|
||||
<user-panel />
|
||||
<div v-if="!isMobileLayout">
|
||||
<nav-panel />
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
||||
<notifications v-if="currentUser && !thirdColumnEnabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="main-scroller"
|
||||
class="column main"
|
||||
:class="{ '-full-height': isChats }"
|
||||
class="main"
|
||||
>
|
||||
<div
|
||||
v-if="!currentUser"
|
||||
|
@ -49,25 +51,34 @@
|
|||
<router-view />
|
||||
</div>
|
||||
<div
|
||||
id="notifs-column"
|
||||
class="column -scrollable"
|
||||
:class="{ '-show-scrollbar': showScrollbars }"
|
||||
/>
|
||||
v-if="thirdColumnEnabled"
|
||||
class="sidebar-flexer mobile-hidden"
|
||||
:style="notifsAlign"
|
||||
>
|
||||
<div class="sidebar-bounds">
|
||||
<div class="sidebar-scroller">
|
||||
<div class="sidebar">
|
||||
<div v-if="!isMobileLayout">
|
||||
<notifications v-if="currentUser" />
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<media-modal />
|
||||
</div>
|
||||
<media-modal />
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
class="floating-shout mobile-hidden"
|
||||
:class="{ '-left': shoutboxPosition }"
|
||||
/>
|
||||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<ModModal />
|
||||
<portal-target name="modal" />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -30,5 +30,3 @@ $fallback--attachmentRadius: 10px;
|
|||
$fallback--chatMessageRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 34 KiB |
|
@ -1,25 +1,14 @@
|
|||
import Cookies from 'js-cookie'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import routes from './routes'
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import App from '../App.vue'
|
||||
import { windowWidth } 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'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
|
||||
import DomNodeToComponent from '../modules/domNodeToComponent.js'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
const parsedInitialResults = () => {
|
||||
|
@ -53,20 +42,6 @@ const preloadFetch = async (request) => {
|
|||
}
|
||||
}
|
||||
|
||||
const resolveLanguage = (instanceLanguages) => {
|
||||
// First language in navigator.languages that is listed as an instance language
|
||||
// falls back to first instance language
|
||||
const navigatorLanguages = navigator.languages.map((x) => x.split('-')[0])
|
||||
|
||||
for (const navLanguage of navigatorLanguages) {
|
||||
if (instanceLanguages.includes(navLanguage)) {
|
||||
return navLanguage
|
||||
}
|
||||
}
|
||||
|
||||
return instanceLanguages[0]
|
||||
}
|
||||
|
||||
const getInstanceConfig = async ({ store }) => {
|
||||
try {
|
||||
const res = await preloadFetch('/api/v1/instance')
|
||||
|
@ -77,10 +52,6 @@ const getInstanceConfig = async ({ store }) => {
|
|||
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
// don't override cookie if set
|
||||
if (!Cookies.get('userLanguage')) {
|
||||
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
|
||||
}
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
|
@ -144,17 +115,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideBotIndication')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('hideSiteName')
|
||||
copyInstanceOption('hideSiteFavicon')
|
||||
copyInstanceOption('showWiderShortcuts')
|
||||
copyInstanceOption('showNavShortcuts')
|
||||
copyInstanceOption('showPanelNavShortcuts')
|
||||
copyInstanceOption('stopGifs')
|
||||
copyInstanceOption('logo')
|
||||
copyInstanceOption('conversationDisplay')
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
|
@ -175,14 +138,15 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('renderMisskeyMarkdown')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
|
@ -278,14 +242,10 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') || features.includes('custom_emoji_reactions') })
|
||||
store.dispatch('setInstanceOption', { name: 'quotePostsAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
|
@ -321,7 +281,6 @@ const getNodeInfo = async ({ store }) => {
|
|||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
|
@ -329,9 +288,6 @@ const getNodeInfo = async ({ store }) => {
|
|||
: federation.enabled
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'publicTimelineVisibility', value: metadata.publicTimelineVisibility })
|
||||
store.dispatch('setInstanceOption', { name: 'federatedTimelineAvailable', value: metadata.federatedTimelineAvailable })
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
|
@ -369,8 +325,8 @@ const checkOAuthToken = async ({ store }) => {
|
|||
}
|
||||
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
store.dispatch('setLayoutWidth', windowWidth())
|
||||
store.dispatch('setLayoutHeight', windowHeight())
|
||||
const width = windowWidth()
|
||||
store.dispatch('setMobileLayout', width <= 800)
|
||||
|
||||
FaviconService.initFaviconService()
|
||||
|
||||
|
@ -406,37 +362,29 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
])
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return {}
|
||||
return false
|
||||
}
|
||||
|
||||
return savedPosition || { left: 0, top: 0 }
|
||||
return savedPosition || { x: 0, y: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(i18n)
|
||||
|
||||
app.use(vClickOutside)
|
||||
app.use(VBodyScrollLock)
|
||||
app.use(DomNodeToComponent)
|
||||
|
||||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
return app
|
||||
/* eslint-disable no-new */
|
||||
return new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
}
|
||||
|
||||
export default afterStoreSetup
|
||||
|
|
|
@ -2,7 +2,6 @@ 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 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'
|
||||
|
@ -21,12 +20,6 @@ import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
|||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListTimeline from 'components/list_timeline/list_timeline.vue'
|
||||
import ListEdit from 'components/list_edit/list_edit.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import RegistrationRequestSent from 'components/registration_request_sent/registration_request_sent.vue'
|
||||
import AwaitingEmailConfirmation from 'components/awaiting_email_confirmation/awaiting_email_confirmation.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
@ -48,13 +41,12 @@ export default (store) => {
|
|||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ 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: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
|
@ -63,27 +55,21 @@ export default (store) => {
|
|||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ 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 },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
||||
{ 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 }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
|
||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||
]
|
||||
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
|
|
|
@ -3,7 +3,6 @@ import FeaturesPanel from '../features_panel/features_panel.vue'
|
|||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import LocalBubblePanel from '../local_bubble_panel/local_bubble_panel.vue'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
@ -11,8 +10,7 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel,
|
||||
LocalBubblePanel
|
||||
MRFTransparencyPanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
|
@ -20,9 +18,6 @@ const About = {
|
|||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
showLocalBubblePanel () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<div class="column-inner">
|
||||
<div class="sidebar">
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<staff-panel />
|
||||
<terms-of-service-panel />
|
||||
<LocalBubblePanel v-if="showLocalBubblePanel" />
|
||||
<MRFTransparencyPanel />
|
||||
<features-panel v-if="showFeaturesPanel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
<script src="./about.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
|
@ -16,25 +15,13 @@ const AccountActions = {
|
|||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingConfirmBlock: false
|
||||
}
|
||||
return { }
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
ConfirmModal
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
refetchRelationship () {
|
||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
|
@ -42,44 +29,22 @@ const AccountActions = {
|
|||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
muteDomain () {
|
||||
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
},
|
||||
unmuteDomain () {
|
||||
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
},
|
||||
openChat () {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
params: { recipient_id: this.user.id }
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
@ -28,13 +28,6 @@
|
|||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<button
|
||||
v-if="relationship.followed_by"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
@ -55,20 +48,6 @@
|
|||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.domain_blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="unmuteDomain"
|
||||
>
|
||||
{{ $t('user_card.domain_muted') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!user.is_local"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="muteDomain"
|
||||
>
|
||||
{{ $t('user_card.mute_domain') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pleromaChatMessagesAvailable"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
@ -78,7 +57,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
@ -87,27 +66,6 @@
|
|||
</button>
|
||||
</template>
|
||||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
:confirm-text="$t('user_card.block_confirm_accept_button')"
|
||||
:cancel-text="$t('user_card.block_confirm_cancel_button')"
|
||||
@accepted="doBlockUser"
|
||||
@cancelled="hideConfirmBlock"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -116,6 +74,10 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.AccountActions {
|
||||
button.dropdown-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ellipsis-button {
|
||||
width: 2.5em;
|
||||
margin: -0.5em 0;
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
import { mapState } from 'vuex'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
const time = this.announcement['published_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt () {
|
||||
const time = this.announcement['starts_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt () {
|
||||
const time = this.announcement['ends_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
||||
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
||||
this.editedAnnouncement.allDay = this.announcement['all_day']
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
|
@ -1,136 +0,0 @@
|
|||
<template>
|
||||
<div class="announcement">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.title') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<rich-content
|
||||
v-if="!editing"
|
||||
:html="content"
|
||||
:emoji="announcement.emojis"
|
||||
:handle-links="true"
|
||||
/>
|
||||
<announcement-editor
|
||||
v-else
|
||||
:announcement="editedAnnouncement"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
</span>
|
||||
<span v-if="endsAt">
|
||||
{{ $t('announcements.end_time_display', { time: endsAt }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="btn button-default"
|
||||
:class="{ toggled: isRead }"
|
||||
:disabled="inactive"
|
||||
:title="inactive ? $t('announcements.inactive_message') : ''"
|
||||
@click="markAsRead"
|
||||
>
|
||||
{{ $t('announcements.mark_as_read_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
class="btn button-default"
|
||||
@click="enterEditMode"
|
||||
>
|
||||
{{ $t('announcements.edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
class="btn button-default"
|
||||
@click="deleteAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.delete_action') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="submitEdit"
|
||||
>
|
||||
{{ $t('announcements.submit_edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ $t('announcements.cancel_edit_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="editing && editError"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.edit_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.times {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.footer .actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
margin: 1em;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,13 +0,0 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
|
@ -1,60 +0,0 @@
|
|||
<template>
|
||||
<div class="announcement-editor">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="announcement.content"
|
||||
class="post-textarea"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:placeholder="$t('announcements.post_placeholder')"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
id="announcement-all-day"
|
||||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement_editor.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.announcement-editor {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.announcement-metadata {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,55 +0,0 @@
|
|||
import { mapState } from 'vuex'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
|
@ -1,79 +0,0 @@
|
|||
<template>
|
||||
<div class="panel panel-default announcements-page">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('announcements.page_header') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
>
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<announcement-editor
|
||||
:announcement="newAnnouncement"
|
||||
:disabled="posting"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
class="btn button-default post-button"
|
||||
:disabled="posting"
|
||||
@click.prevent="postAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.post_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.post_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.post-button {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
|
@ -10,14 +11,8 @@ import {
|
|||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Blurhash from '../blurhash/Blurhash.vue'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
@ -25,67 +20,37 @@ library.add(
|
|||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faTimes
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
props: [
|
||||
'attachment',
|
||||
'description',
|
||||
'hideDescription',
|
||||
'nsfw',
|
||||
'size',
|
||||
'allowPlay',
|
||||
'setMedia',
|
||||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit'
|
||||
'naturalSizeLoad'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false
|
||||
showHidden: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
Blurhash
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined
|
||||
},
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
useBlurhash () {
|
||||
return this.$store.getters.mergedConfig.useBlurhash
|
||||
return this.size === 'hide' || this.type === 'unknown'
|
||||
},
|
||||
placeholderName () {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
|
@ -109,39 +74,24 @@ const Attachment = {
|
|||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
|
||||
},
|
||||
isSmall () {
|
||||
return this.size === 'small'
|
||||
},
|
||||
fullwidth () {
|
||||
if (this.size === 'hide') return false
|
||||
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
|
||||
},
|
||||
useModal () {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
case 'small':
|
||||
modalTypes = ['image', 'video', 'audio', 'flash']
|
||||
break
|
||||
default:
|
||||
modalTypes = this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video', 'flash']
|
||||
: ['image']
|
||||
break
|
||||
}
|
||||
const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
|
||||
: this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag () {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
statusForm () {
|
||||
return this.$parent.$parent
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
|
@ -150,37 +100,12 @@ const Attachment = {
|
|||
},
|
||||
openModal (event) {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
} else if (this.type === 'unknown') {
|
||||
window.open(this.attachment.url)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.setMedia()
|
||||
this.$store.dispatch('setCurrent', this.attachment)
|
||||
}
|
||||
},
|
||||
openModalForce (event) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
},
|
||||
onEdit (event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove () {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp () {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn () {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash () {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded (event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription () {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden (event) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
|
@ -207,30 +132,7 @@ const Attachment = {
|
|||
onImageLoad (image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
resize (e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
|
||||
// Reset to default height for empty form, nothing else to do here.
|
||||
if (target.value === '') {
|
||||
target.style.height = null
|
||||
this.$emit('resize')
|
||||
return
|
||||
}
|
||||
|
||||
const paddingString = getComputedStyle(target)['padding']
|
||||
// remove -px suffix
|
||||
const padding = Number(paddingString.substring(0, paddingString.length - 2))
|
||||
|
||||
target.style.height = 'auto'
|
||||
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
||||
target.style.height = `${newHeight}px`
|
||||
this.$emit('resize', newHeight)
|
||||
},
|
||||
postStatus (event) {
|
||||
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
||||
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,283 +0,0 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.Attachment {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
align-self: flex-start;
|
||||
line-height: 0;
|
||||
height: 100%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: $fallback--attachmentRadius;
|
||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
||||
.attachment-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.description-container {
|
||||
flex: 0 1 0;
|
||||
display: flex;
|
||||
padding-top: 0.5em;
|
||||
z-index: 1;
|
||||
max-height: 50%;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-top: 0;
|
||||
background: var(--popover);
|
||||
box-shadow: var(--popupShadow);
|
||||
}
|
||||
}
|
||||
|
||||
.description-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
box-sizing: content-box;
|
||||
overflow: hidden;
|
||||
transition: min-height 200ms 100ms;
|
||||
|
||||
padding-bottom: var(--_padding);
|
||||
height: calc(var(--post-line-height) * 1em);
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& .placeholder-container,
|
||||
& .image-container,
|
||||
& .audio-container,
|
||||
& .video-container,
|
||||
& .flash-container,
|
||||
& .oembed-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& .flash-container,
|
||||
& .video-container {
|
||||
& .flash,
|
||||
& video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-buttons {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
z-index: 1;
|
||||
|
||||
.attachment-button {
|
||||
padding: 0;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
margin-left: 0.5em;
|
||||
font-size: 1.25em;
|
||||
// TODO: theming? hard to theme with unknown background image color
|
||||
background: rgba(230, 230, 230, 0.7);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oembed-container {
|
||||
line-height: 1.2em;
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex: 1;
|
||||
img {
|
||||
border: 0px;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-size-small {
|
||||
.play-icon {
|
||||
zoom: 0.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-buttons {
|
||||
zoom: 0.7;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.-editable {
|
||||
padding: 0.5em;
|
||||
|
||||
& .description-container,
|
||||
& .attachment-buttons {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.-placeholder {
|
||||
display: inline-block;
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
|
||||
&:not(.-editable) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.-editable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
& .description-container,
|
||||
& .attachment-buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.description-container {
|
||||
flex: 1;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.attachment-buttons {
|
||||
order: 99;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.-loading {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
&.-contain-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.-cover-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
<div
|
||||
v-if="usePlaceholder"
|
||||
class="Attachment -placeholder button-unstyled"
|
||||
:class="classNames"
|
||||
:class="{ 'fullwidth': fullwidth }"
|
||||
@click="openModal"
|
||||
>
|
||||
<a
|
||||
|
@ -12,267 +11,318 @@
|
|||
:href="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
@click.prevent
|
||||
>
|
||||
<FAIcon :icon="placeholderIconClass" />
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
|
||||
</a>
|
||||
<div
|
||||
v-if="edit || remove"
|
||||
class="attachment-buttons"
|
||||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-unstyled attachment-button"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<input
|
||||
v-if="edit"
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="Attachment"
|
||||
:class="classNames"
|
||||
v-show="!isEmpty"
|
||||
class="attachment"
|
||||
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
|
||||
>
|
||||
<div
|
||||
v-show="!isEmpty"
|
||||
class="attachment-wrapper"
|
||||
<a
|
||||
v-if="hidden"
|
||||
class="image-attachment"
|
||||
:href="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
>
|
||||
<a
|
||||
v-if="hidden"
|
||||
class="image-container"
|
||||
:href="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
<img
|
||||
:key="nsfwImage"
|
||||
class="nsfw"
|
||||
:src="nsfwImage"
|
||||
:class="{'small': isSmall}"
|
||||
>
|
||||
<Blurhash
|
||||
v-if="useBlurhash && attachment.blurhash"
|
||||
:height="512"
|
||||
:width="1024"
|
||||
:hash="attachment.blurhash"
|
||||
:punch="1"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:key="nsfwImage"
|
||||
class="nsfw"
|
||||
:src="nsfwImage"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
class="attachment-buttons"
|
||||
>
|
||||
<button
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
<FAIcon icon="search-plus" />
|
||||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
<FAIcon icon="chevron-left" />
|
||||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
<FAIcon icon="chevron-right" />
|
||||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<StillImage
|
||||
class="image"
|
||||
:referrerpolicy="referrerpolicy"
|
||||
:mimetype="attachment.mimetype"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
:image-load-handler="onImageLoad"
|
||||
:alt="attachment.description"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="type === 'unknown' && !hidden"
|
||||
class="placeholder-container"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
>
|
||||
<FAIcon
|
||||
size="5x"
|
||||
:icon="placeholderIconClass"
|
||||
/>
|
||||
<p>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<component
|
||||
:is="videoTag"
|
||||
v-if="type === 'video' && !hidden"
|
||||
class="video-container"
|
||||
:class="{ 'button-unstyled': 'isModal' }"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<VideoAttachment
|
||||
class="video"
|
||||
:attachment="attachment"
|
||||
:controls="!useModal"
|
||||
@play="$emit('play')"
|
||||
@pause="$emit('pause')"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="useModal"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
</component>
|
||||
|
||||
<span
|
||||
v-if="type === 'audio' && !hidden"
|
||||
class="audio-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<audio
|
||||
v-if="type === 'audio'"
|
||||
:src="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
controls
|
||||
@play="$emit('play')"
|
||||
@pause="$emit('pause')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="type === 'html' && attachment.oembed"
|
||||
class="oembed-container"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
<div
|
||||
v-if="attachment.thumb_url"
|
||||
class="image"
|
||||
>
|
||||
<img :src="attachment.thumb_url">
|
||||
</div>
|
||||
<div class="text">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="type === 'flash' && !hidden"
|
||||
class="flash-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<Flash
|
||||
ref="flash"
|
||||
class="flash"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
@playerOpened="setFlashLoaded(true)"
|
||||
@playerClosed="setFlashLoaded(false)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<textarea
|
||||
v-if="edit"
|
||||
v-model="localDescription"
|
||||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
rows="1"
|
||||
cols="1"
|
||||
@keydown.ctrl.enter="postStatus()"
|
||||
@input="resize"
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</a>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal && !hidden"
|
||||
class="button-unstyled hider"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
|
||||
<a
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-attachment"
|
||||
:class="{'hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
@click="openModal"
|
||||
>
|
||||
<StillImage
|
||||
class="image"
|
||||
:referrerpolicy="referrerpolicy"
|
||||
:mimetype="attachment.mimetype"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
:image-load-handler="onImageLoad"
|
||||
:alt="attachment.description"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="type === 'video' && !hidden"
|
||||
class="video-container"
|
||||
:class="{'small': isSmall}"
|
||||
:href="allowPlay ? undefined : attachment.url"
|
||||
@click="openModal"
|
||||
>
|
||||
<VideoAttachment
|
||||
class="video"
|
||||
:attachment="attachment"
|
||||
:controls="allowPlay"
|
||||
@play="$emit('play')"
|
||||
@pause="$emit('pause')"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="!allowPlay"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<audio
|
||||
v-if="type === 'audio'"
|
||||
:src="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
controls
|
||||
@play="$emit('play')"
|
||||
@pause="$emit('pause')"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="type === 'html' && attachment.oembed"
|
||||
class="oembed"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
<div
|
||||
v-if="attachment.thumb_url"
|
||||
class="image"
|
||||
>
|
||||
<img :src="attachment.thumb_url">
|
||||
</div>
|
||||
<div class="text">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Flash
|
||||
v-if="type === 'flash'"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./attachment.js"></script>
|
||||
|
||||
<style src="./attachment.scss" lang="scss"></style>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.non-gallery {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
padding: 0.3em 1em 0.3em 0;
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.nsfw-placeholder {
|
||||
cursor: pointer;
|
||||
|
||||
&.loading {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment {
|
||||
position: relative;
|
||||
margin-top: 0.5em;
|
||||
align-self: flex-start;
|
||||
line-height: 0;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: $fallback--attachmentRadius;
|
||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.non-gallery.attachment {
|
||||
&.flash,
|
||||
&.video {
|
||||
flex: 1 0 40%;
|
||||
}
|
||||
.nsfw {
|
||||
height: 260px;
|
||||
}
|
||||
.small {
|
||||
height: 120px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.video {
|
||||
height: 260px;
|
||||
display: flex;
|
||||
}
|
||||
video {
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.fullwidth {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
// fixes small gap below video
|
||||
&.video {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.play-icon::before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.html {
|
||||
flex-basis: 90%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hider {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
z-index: 4;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
font-size: 1.25em;
|
||||
// TODO: theming? hard to theme with unknown background image color
|
||||
background: rgba(230, 230, 230, 0.7);
|
||||
.svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
&:hover .svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img.media-upload {
|
||||
line-height: 0;
|
||||
max-height: 200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.oembed {
|
||||
line-height: 1.2em;
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex: 1;
|
||||
img {
|
||||
border: 0px;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-attachment {
|
||||
&,
|
||||
& .image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
image-orientation: from-image; // NOTE: only FF supports this
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { h, resolveComponent } from 'vue'
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
|
@ -6,8 +5,8 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render () {
|
||||
return h(resolveComponent(this.authForm))
|
||||
render (createElement) {
|
||||
return createElement('component', { is: this.authForm })
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./avatar_list.js"></script>
|
||||
<script src="./avatar_list.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export default {
|
||||
computed: {
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{{ $t('registration.awaiting_email_confirmation_title') }}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{{ $t('registration.awaiting_email_confirmation') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./awaiting_email_confirmation.js"></script>
|
|
@ -1,6 +1,5 @@
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
|
@ -14,8 +13,7 @@ const BasicUserCard = {
|
|||
},
|
||||
components: {
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
RichContent
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<UserAvatar
|
||||
class="avatar"
|
||||
:user="user"
|
||||
@click.prevent="toggleUserExpanded"
|
||||
@click.prevent.native="toggleUserExpanded"
|
||||
/>
|
||||
</router-link>
|
||||
<div
|
||||
|
@ -25,11 +25,17 @@
|
|||
:title="user.name"
|
||||
class="basic-user-card-user-name"
|
||||
>
|
||||
<RichContent
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="user.name_html"
|
||||
class="basic-user-card-user-name-value"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
v-html="user.name_html"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="basic-user-card-user-name-value"
|
||||
>{{ user.name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
<template>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="blurhash"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { decode } from "blurhash";
|
||||
|
||||
export default {
|
||||
name: 'Blurhash',
|
||||
props: {
|
||||
hash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
punch: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.canvas = this.$refs.canvas;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.canvas.width = 1024;
|
||||
this.canvas.height = 512;
|
||||
this.draw();
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
const pixels = decode(this.hash, this.width, this.height, this.punch);
|
||||
const imageData = this.ctx.createImageData(this.width, this.height);
|
||||
imageData.data.set(pixels);
|
||||
this.ctx.putImageData(imageData, 0, 0);
|
||||
fetch("/static/blurhash-overlay.png")
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(blob);
|
||||
img.onload = () => {
|
||||
this.ctx.drawImage(img, 0, 0, this.width, this.height);
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -9,7 +9,7 @@ const Bookmarks = {
|
|||
components: {
|
||||
Timeline
|
||||
},
|
||||
unmounted () {
|
||||
destroyed () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
const PublicTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PublicTimeline
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.bubble_timeline')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bubble'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./bubble_timeline.js"></script>
|
|
@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
|
|||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronDown,
|
||||
|
@ -20,7 +20,7 @@ library.add(
|
|||
)
|
||||
|
||||
const BOTTOMED_OUT_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||
const MARK_AS_READ_DELAY = 1500
|
||||
const MAX_RETRIES = 10
|
||||
|
@ -43,7 +43,7 @@ const Chat = {
|
|||
},
|
||||
created () {
|
||||
this.startFetching()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
window.addEventListener('resize', this.handleLayoutChange)
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
|
@ -52,11 +52,15 @@ const Chat = {
|
|||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
this.handleResize()
|
||||
})
|
||||
this.setChatLayout()
|
||||
},
|
||||
unmounted () {
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleLayoutChange)
|
||||
this.unsetChatLayout()
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
|
@ -92,7 +96,8 @@ const Chat = {
|
|||
...mapState({
|
||||
backendInteractor: state => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
||||
mobileLayout: state => state.interface.layoutType === 'mobile',
|
||||
mobileLayout: state => state.interface.mobileLayout,
|
||||
layoutHeight: state => state.interface.layoutHeight,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
|
@ -110,6 +115,9 @@ const Chat = {
|
|||
'$route': function () {
|
||||
this.startFetching()
|
||||
},
|
||||
layoutHeight () {
|
||||
this.handleResize({ expand: true })
|
||||
},
|
||||
mastoUserSocketStatus (newValue) {
|
||||
if (newValue === WSConnectionStatus.JOINED) {
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
|
@ -124,6 +132,7 @@ const Chat = {
|
|||
onFilesDropped () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
this.updateScrollableContainerHeight()
|
||||
})
|
||||
},
|
||||
handleVisibilityChange () {
|
||||
|
@ -133,7 +142,43 @@ const Chat = {
|
|||
}
|
||||
})
|
||||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
setChatLayout () {
|
||||
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
|
||||
// This layout prevents empty spaces from being visible at the bottom
|
||||
// of the chat on iOS Safari (`safe-area-inset`) when
|
||||
// - the on-screen keyboard appears and the user starts typing
|
||||
// - the user selects the text inside the input area
|
||||
// - the user selects and deletes the text that is multiple lines long
|
||||
// TODO: unify the chat layout with the global layout.
|
||||
let html = document.querySelector('html')
|
||||
if (html) {
|
||||
html.classList.add('chat-layout')
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
})
|
||||
},
|
||||
unsetChatLayout () {
|
||||
let html = document.querySelector('html')
|
||||
if (html) {
|
||||
html.classList.remove('chat-layout')
|
||||
}
|
||||
},
|
||||
handleLayoutChange () {
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
this.scrollDown()
|
||||
})
|
||||
},
|
||||
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
|
||||
updateScrollableContainerHeight () {
|
||||
const header = this.$refs.header
|
||||
const footer = this.$refs.footer
|
||||
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
|
||||
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
|
||||
},
|
||||
// Preserves the scroll position when OSK appears or the posting form changes its height.
|
||||
handleResize (opts = {}) {
|
||||
const { expand = false, delayed = false } = opts
|
||||
|
||||
|
@ -145,20 +190,29 @@ const Chat = {
|
|||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight = undefined } = getScrollPosition()
|
||||
this.updateScrollableContainerHeight()
|
||||
|
||||
const { offsetHeight = undefined } = this.lastScrollPosition
|
||||
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
|
||||
|
||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
||||
if (diff !== 0 || (!this.bottomedOut() && expand)) {
|
||||
if (diff < 0 || (!this.bottomedOut() && expand)) {
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: window.scrollY + diff })
|
||||
this.updateScrollableContainerHeight()
|
||||
this.$refs.scrollable.scrollTo({
|
||||
top: this.$refs.scrollable.scrollTop - diff,
|
||||
left: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
scrollDown (options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
const scrollable = this.$refs.scrollable
|
||||
if (!scrollable) { return }
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
|
||||
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
|
@ -174,10 +228,11 @@ const Chat = {
|
|||
})
|
||||
},
|
||||
bottomedOut (offset) {
|
||||
return isBottomedOut(offset)
|
||||
return isBottomedOut(this.$refs.scrollable, offset)
|
||||
},
|
||||
reachedTop () {
|
||||
return window.scrollY <= 0
|
||||
const scrollable = this.$refs.scrollable
|
||||
return scrollable && scrollable.scrollTop <= 0
|
||||
},
|
||||
cullOlderCheck () {
|
||||
window.setTimeout(() => {
|
||||
|
@ -208,9 +263,10 @@ const Chat = {
|
|||
}
|
||||
}, 200),
|
||||
handleScrollUp (positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
|
||||
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
||||
this.$refs.scrollable.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
left: 0
|
||||
})
|
||||
},
|
||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
|
@ -229,18 +285,22 @@ const Chat = {
|
|||
chatService.clear(chatMessageService)
|
||||
}
|
||||
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
|
||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
|
||||
if (isFirstFetch) {
|
||||
this.updateScrollableContainerHeight()
|
||||
}
|
||||
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of the scrollable container.
|
||||
// If this is the case, we want to fetch the messages until the scrollable container
|
||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
}
|
||||
})
|
||||
|
@ -276,6 +336,9 @@ const Chat = {
|
|||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
// to account for the potential delay in the DOM update.
|
||||
setTimeout(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
}, SAFE_RESIZE_TIME_OFFSET)
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
.chat-view {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
width: 100%;
|
||||
|
||||
.chat-title {
|
||||
// prevents chat header jumping on when the user avatar loads
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.chat-view-inner {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
margin: 0.5em 0.5em 0 0.5em;
|
||||
}
|
||||
|
||||
.chat-view-body {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--chatBg, $fallback--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
min-height: calc(100vh - var(--navbar-height));
|
||||
min-height: 100%;
|
||||
margin: 0 0 0 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
||||
|
@ -26,32 +32,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
.scrollable-message-list {
|
||||
padding: 0 0.8em;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-view-heading {
|
||||
grid-template-columns: auto minmax(50%, 1fr);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
top: 50px;
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
position: sticky;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.go-back-button {
|
||||
cursor: pointer;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
width: var(--__panel-heading-height-inner);
|
||||
padding: 0.6em;
|
||||
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||
}
|
||||
|
||||
.jump-to-bottom-button {
|
||||
|
@ -105,4 +115,56 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-view-inner {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.chat-view-body {
|
||||
display: flex;
|
||||
min-height: auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-view-heading {
|
||||
box-sizing: border-box;
|
||||
position: static;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
|
||||
/* This practically overlays the panel heading color over panel background
|
||||
* color. This is needed because we allow transparent panel background and
|
||||
* it doesn't work well in this "disjointed panel header" case
|
||||
*/
|
||||
background:
|
||||
linear-gradient(to top, var(--panel), var(--panel)),
|
||||
linear-gradient(to top, var(--bg), var(--bg));
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.scrollable-message-list {
|
||||
display: unset;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,23 @@
|
|||
<div class="chat-view">
|
||||
<div class="chat-view-inner">
|
||||
<div
|
||||
id="nav"
|
||||
ref="inner"
|
||||
class="panel-default panel chat-view-body"
|
||||
>
|
||||
<div
|
||||
ref="header"
|
||||
class="panel-heading -sticky chat-view-heading"
|
||||
class="panel-heading chat-view-heading mobile-hidden"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled go-back-button"
|
||||
<a
|
||||
class="go-back-button"
|
||||
@click="goBack"
|
||||
>
|
||||
<FAIcon
|
||||
size="lg"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
<div class="title text-center">
|
||||
<ChatTitle
|
||||
:user="recipient"
|
||||
|
@ -25,69 +26,73 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="message-list"
|
||||
:style="{ height: scrollableContainerHeight }"
|
||||
>
|
||||
<template v-if="!errorLoadingChat">
|
||||
<ChatMessage
|
||||
v-for="chatViewItem in chatViewItems"
|
||||
:key="chatViewItem.id"
|
||||
:author="recipient"
|
||||
:chat-view-item="chatViewItem"
|
||||
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
||||
@hover="onMessageHover"
|
||||
/>
|
||||
</template>
|
||||
<template>
|
||||
<div
|
||||
v-else
|
||||
class="chat-loading-error"
|
||||
ref="scrollable"
|
||||
class="scrollable-message-list"
|
||||
:style="{ height: scrollableContainerHeight }"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="alert error">
|
||||
{{ $t('chats.error_loading_chat') }}
|
||||
<template v-if="!errorLoadingChat">
|
||||
<ChatMessage
|
||||
v-for="chatViewItem in chatViewItems"
|
||||
:key="chatViewItem.id"
|
||||
:author="recipient"
|
||||
:chat-view-item="chatViewItem"
|
||||
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
||||
@hover="onMessageHover"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="chat-loading-error"
|
||||
>
|
||||
<div class="alert error">
|
||||
{{ $t('chats.error_loading_chat') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="footer"
|
||||
class="panel-body footer"
|
||||
>
|
||||
<div
|
||||
class="jump-to-bottom-button"
|
||||
:class="{ 'visible': jumpToBottomButtonVisible }"
|
||||
@click="scrollDown({ behavior: 'smooth' })"
|
||||
ref="footer"
|
||||
class="panel-body footer"
|
||||
>
|
||||
<span>
|
||||
<FAIcon icon="chevron-down" />
|
||||
<div
|
||||
v-if="newMessageCount"
|
||||
class="badge badge-notification unread-chat-count unread-message-count"
|
||||
>
|
||||
{{ newMessageCount }}
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="jump-to-bottom-button"
|
||||
:class="{ 'visible': jumpToBottomButtonVisible }"
|
||||
@click="scrollDown({ behavior: 'smooth' })"
|
||||
>
|
||||
<span>
|
||||
<FAIcon icon="chevron-down" />
|
||||
<div
|
||||
v-if="newMessageCount"
|
||||
class="badge badge-notification unread-chat-count unread-message-count"
|
||||
>
|
||||
{{ newMessageCount }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<PostStatusForm
|
||||
:disable-subject="true"
|
||||
:disable-scope-selector="true"
|
||||
:disable-notice="true"
|
||||
:disable-lock-warning="true"
|
||||
:disable-polls="true"
|
||||
:disable-sensitivity-checkbox="true"
|
||||
:disable-submit="errorLoadingChat || !currentChat"
|
||||
:disable-preview="true"
|
||||
:optimistic-posting="true"
|
||||
:post-handler="sendMessage"
|
||||
:submit-on-enter="!mobileLayout"
|
||||
:preserve-focus="!mobileLayout"
|
||||
:auto-focus="!mobileLayout"
|
||||
:placeholder="formPlaceholder"
|
||||
:file-limit="1"
|
||||
max-height="160"
|
||||
emoji-picker-placement="top"
|
||||
@resize="handleResize"
|
||||
/>
|
||||
</div>
|
||||
<PostStatusForm
|
||||
:disable-subject="true"
|
||||
:disable-scope-selector="true"
|
||||
:disable-notice="true"
|
||||
:disable-lock-warning="true"
|
||||
:disable-polls="true"
|
||||
:disable-sensitivity-checkbox="true"
|
||||
:disable-submit="errorLoadingChat || !currentChat"
|
||||
:disable-preview="true"
|
||||
:optimistic-posting="true"
|
||||
:post-handler="sendMessage"
|
||||
:submit-on-enter="!mobileLayout"
|
||||
:preserve-focus="!mobileLayout"
|
||||
:auto-focus="!mobileLayout"
|
||||
:placeholder="formPlaceholder"
|
||||
:file-limit="1"
|
||||
max-height="160"
|
||||
emoji-picker-placement="top"
|
||||
@resize="handleResize"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
const ChatLayout = {
|
||||
methods: {
|
||||
setChatLayout () {
|
||||
let body = document.querySelector('body')
|
||||
if (body) {
|
||||
body.style.overscrollBehavior = 'none'
|
||||
}
|
||||
if (this.isMobileLayout) {
|
||||
this.setMobileChatLayout()
|
||||
}
|
||||
},
|
||||
unsetChatLayout () {
|
||||
this.unsetMobileChatLayout()
|
||||
let body = document.querySelector('body')
|
||||
if (body) {
|
||||
body.style.overscrollBehavior = 'unset'
|
||||
}
|
||||
},
|
||||
setMobileChatLayout () {
|
||||
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
|
||||
// This layout prevents empty spaces from being visible at the bottom
|
||||
// of the chat on iOS Safari (`safe-area-inset`) when
|
||||
// - the on-screen keyboard appears and the user starts typing
|
||||
// - the user selects the text inside the input area
|
||||
// - the user selects and deletes the text that is multiple lines long
|
||||
// TODO: unify the chat layout with the global layout.
|
||||
|
||||
let html = document.querySelector('html')
|
||||
if (html) {
|
||||
html.style.overflow = 'hidden'
|
||||
html.style.height = '100%'
|
||||
}
|
||||
|
||||
let body = document.querySelector('body')
|
||||
if (body) {
|
||||
body.style.height = '100%'
|
||||
}
|
||||
|
||||
let app = document.getElementById('app')
|
||||
if (app) {
|
||||
app.style.height = '100%'
|
||||
app.style.overflow = 'hidden'
|
||||
app.style.minHeight = 'auto'
|
||||
}
|
||||
|
||||
let appBgWrapper = window.document.getElementById('app_bg_wrapper')
|
||||
if (appBgWrapper) {
|
||||
appBgWrapper.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
let main = document.getElementsByClassName('main')[0]
|
||||
if (main) {
|
||||
main.style.overflow = 'hidden'
|
||||
main.style.height = '100%'
|
||||
}
|
||||
|
||||
let content = document.getElementById('content')
|
||||
if (content) {
|
||||
content.style.paddingTop = '0'
|
||||
content.style.height = '100%'
|
||||
content.style.overflow = 'visible'
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateSize()
|
||||
})
|
||||
},
|
||||
unsetMobileChatLayout () {
|
||||
let html = document.querySelector('html')
|
||||
if (html) {
|
||||
html.style.overflow = 'visible'
|
||||
html.style.height = 'unset'
|
||||
}
|
||||
|
||||
let body = document.querySelector('body')
|
||||
if (body) {
|
||||
body.style.height = 'unset'
|
||||
}
|
||||
|
||||
let app = document.getElementById('app')
|
||||
if (app) {
|
||||
app.style.height = '100%'
|
||||
app.style.overflow = 'visible'
|
||||
app.style.minHeight = '100vh'
|
||||
}
|
||||
|
||||
let appBgWrapper = document.getElementById('app_bg_wrapper')
|
||||
if (appBgWrapper) {
|
||||
appBgWrapper.style.overflow = 'visible'
|
||||
}
|
||||
|
||||
let main = document.getElementsByClassName('main')[0]
|
||||
if (main) {
|
||||
main.style.overflow = 'visible'
|
||||
main.style.height = 'unset'
|
||||
}
|
||||
|
||||
let content = document.getElementById('content')
|
||||
if (content) {
|
||||
content.style.paddingTop = '60px'
|
||||
content.style.height = 'unset'
|
||||
content.style.overflow = 'unset'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatLayout
|
|
@ -1,9 +1,9 @@
|
|||
// Captures a scroll position
|
||||
export const getScrollPosition = () => {
|
||||
export const getScrollPosition = (el) => {
|
||||
return {
|
||||
scrollTop: window.scrollY,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
offsetHeight: window.innerHeight
|
||||
scrollTop: el.scrollTop,
|
||||
scrollHeight: el.scrollHeight,
|
||||
offsetHeight: el.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,21 @@ export const getNewTopPosition = (previousPosition, newPosition) => {
|
|||
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
}
|
||||
|
||||
export const isBottomedOut = (offset = 0) => {
|
||||
const scrollHeight = window.scrollY + offset
|
||||
const totalHeight = document.documentElement.scrollHeight - window.innerHeight
|
||||
export const isBottomedOut = (el, offset = 0) => {
|
||||
if (!el) { return }
|
||||
const scrollHeight = el.scrollTop + offset
|
||||
const totalHeight = el.scrollHeight - el.offsetHeight
|
||||
return totalHeight <= scrollHeight
|
||||
}
|
||||
// Returns whether or not the scrollbar is visible.
|
||||
export const isScrollable = () => {
|
||||
return document.documentElement.scrollHeight > window.innerHeight
|
||||
|
||||
// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
|
||||
export const scrollableContainerHeight = (inner, header, footer) => {
|
||||
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
||||
}
|
||||
|
||||
// Returns whether or not the scrollbar is visible.
|
||||
export const isScrollable = (el) => {
|
||||
if (!el) return
|
||||
|
||||
return el.scrollHeight > el.clientHeight
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const ChatAvatar = {
|
||||
props: ['users', 'fallbackUser', 'width', 'height'],
|
||||
components: {
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
getUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
firstUser () {
|
||||
return this.users[0] || this.fallbackUser
|
||||
},
|
||||
secondUser () {
|
||||
return this.users[1]
|
||||
},
|
||||
thirdUser () {
|
||||
return this.users[2]
|
||||
},
|
||||
fourthUser () {
|
||||
return this.users[3]
|
||||
},
|
||||
...mapState({
|
||||
betterShadow: state => state.interface.browserSupport.cssFilter
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatAvatar
|
|
@ -1,130 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="firstUser && secondUser"
|
||||
class="direct-conversation-multi-user-avatar"
|
||||
:style="{ 'width': width, 'height': height }"
|
||||
>
|
||||
<StillImage
|
||||
v-if="fourthUser"
|
||||
class="avatar avatar-fourth direct-conversation-avatar"
|
||||
:alt="fourthUser.screen_name"
|
||||
:title="fourthUser.screen_name"
|
||||
:src="fourthUser.profile_image_url_original"
|
||||
error-src="/images/avi.png"
|
||||
:class="{ 'better-shadow': betterShadow }"
|
||||
/>
|
||||
<StillImage
|
||||
v-if="thirdUser"
|
||||
class="avatar avatar-third direct-conversation-avatar"
|
||||
:alt="thirdUser.screen_name"
|
||||
:title="thirdUser.screen_name"
|
||||
:src="thirdUser.profile_image_url_original"
|
||||
error-src="/images/avi.png"
|
||||
:class="{ 'better-shadow': betterShadow }"
|
||||
/>
|
||||
<StillImage
|
||||
class="avatar avatar-second direct-conversation-avatar"
|
||||
:alt="secondUser.screen_name"
|
||||
:title="secondUser.screen_name"
|
||||
:src="secondUser.profile_image_url_original"
|
||||
error-src="/images/avi.png"
|
||||
:class="{ 'better-shadow': betterShadow }"
|
||||
:style="{ 'height': fourthUser ? '50%' : '100%' }"
|
||||
/>
|
||||
<StillImage
|
||||
class="avatar avatar-first direct-conversation-avatar"
|
||||
:alt="firstUser.screen_name"
|
||||
:title="firstUser.screen_name"
|
||||
:src="firstUser.profile_image_url_original"
|
||||
error-src="/images/avi.png"
|
||||
:class="{ 'better-shadow': betterShadow }"
|
||||
:style="{ 'height': thirdUser ? '50%' : '100%' }"
|
||||
/>
|
||||
</div>
|
||||
<router-link
|
||||
v-else
|
||||
:to="getUserProfileLink(firstUser)"
|
||||
>
|
||||
<StillImage
|
||||
:style="{ 'width': width, 'height': height }"
|
||||
class="avatar direct-conversation-avatar single-user"
|
||||
:alt="firstUser.screen_name"
|
||||
:title="firstUser.screen_name"
|
||||
:src="firstUser.profile_image_url_original"
|
||||
error-src="/images/avi.png"
|
||||
:class="{ 'better-shadow': betterShadow }"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script src="./chat_avatar.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.direct-conversation-multi-user-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar.still-image {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-radius: 0;
|
||||
img, canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.avatar-first {
|
||||
float: right;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.avatar-second {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.avatar-third {
|
||||
float: right;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&.avatar-fourth {
|
||||
float: right;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.direct-conversation-avatar {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
&.single-user {
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
.avatar.still-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: 0;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,7 @@
|
|||
v-else
|
||||
class="chat-list panel panel-default"
|
||||
>
|
||||
<div class="panel-heading -sticky">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t("chats.chats") }}
|
||||
</span>
|
||||
|
@ -23,7 +23,7 @@
|
|||
class="timeline"
|
||||
>
|
||||
<List :items="sortedChatList">
|
||||
<template #item="{item}">
|
||||
<template v-slot:item="{item}">
|
||||
<ChatListItem
|
||||
:key="item.id"
|
||||
:compact="false"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
|
@ -16,7 +16,7 @@ const ChatListItem = {
|
|||
AvatarList,
|
||||
Timeago,
|
||||
ChatTitle,
|
||||
StatusBody
|
||||
StatusContent
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -38,14 +38,12 @@ const ChatListItem = {
|
|||
},
|
||||
messageForStatusContent () {
|
||||
const message = this.chat.lastMessage
|
||||
const messageEmojis = message ? message.emojis : []
|
||||
const isYou = message && message.account_id === this.currentUser.id
|
||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||
return {
|
||||
summary: '',
|
||||
emojis: messageEmojis,
|
||||
raw_html: messagePreview,
|
||||
statusnet_html: messagePreview,
|
||||
text: messagePreview,
|
||||
attachments: []
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
line-height: var(--post-line-height);
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.chat-preview {
|
||||
|
@ -77,15 +77,18 @@
|
|||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
.chat-preview-body {
|
||||
--emoji-size: 1.4em;
|
||||
.StatusContent {
|
||||
img.emoji {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
line-height: var(--post-line-height);
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.chat-preview-body {
|
||||
.single-line {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="chat-preview">
|
||||
<StatusBody
|
||||
class="chat-preview-body"
|
||||
<StatusContent
|
||||
:status="messageForStatusContent"
|
||||
:single-line="true"
|
||||
/>
|
||||
|
|
|
@ -27,7 +27,6 @@ const ChatMessage = {
|
|||
'chatViewItem',
|
||||
'hoveredMessageChain'
|
||||
],
|
||||
emits: ['hover'],
|
||||
components: {
|
||||
Popover,
|
||||
Attachment,
|
||||
|
@ -58,9 +57,8 @@ const ChatMessage = {
|
|||
messageForStatusContent () {
|
||||
return {
|
||||
summary: '',
|
||||
emojis: this.message.emojis,
|
||||
raw_html: this.message.content || '',
|
||||
text: this.message.content || '',
|
||||
statusnet_html: this.message.content,
|
||||
text: this.message.content,
|
||||
attachments: this.message.attachments
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.chat-message-wrapper {
|
||||
|
||||
&.hovered-message-chain {
|
||||
.animated.Avatar {
|
||||
canvas {
|
||||
|
@ -41,12 +40,6 @@
|
|||
.chat-message {
|
||||
display: flex;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
.status-body:hover {
|
||||
--_still-image-img-visibility: visible;
|
||||
--_still-image-canvas-visibility: hidden;
|
||||
--_still-image-label-visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
|
@ -69,6 +62,10 @@
|
|||
&.with-media {
|
||||
width: 100%;
|
||||
|
||||
.gallery-row {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -92,9 +89,8 @@
|
|||
}
|
||||
|
||||
.without-attachment {
|
||||
.message-content {
|
||||
// TODO figure out how to do it properly
|
||||
.RichContent::after {
|
||||
.status-content {
|
||||
&::after {
|
||||
margin-right: 5.4em;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
|
@ -166,7 +162,6 @@
|
|||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-message-date-separator {
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
@show="menuOpened = true"
|
||||
@close="menuOpened = false"
|
||||
>
|
||||
<template #content>
|
||||
<template v-slot:content>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -60,7 +60,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<button
|
||||
class="button-default menu-icon"
|
||||
:title="$t('chats.more')"
|
||||
|
@ -71,11 +71,10 @@
|
|||
</Popover>
|
||||
</div>
|
||||
<StatusContent
|
||||
class="message-content"
|
||||
:status="messageForStatusContent"
|
||||
:full-content="true"
|
||||
>
|
||||
<template #footer>
|
||||
<template v-slot:footer>
|
||||
<span
|
||||
class="created-at"
|
||||
>
|
||||
|
@ -96,7 +95,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./chat_message.js"></script>
|
||||
<script src="./chat_message.js" ></script>
|
||||
<style lang="scss">
|
||||
@import './chat_message.scss';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export default {
|
|||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale.split('_')[0]), { day: 'numeric', month: 'long' })
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
}
|
||||
|
||||
.go-back-button {
|
||||
cursor: pointer;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
width: var(--__panel-heading-height-inner);
|
||||
padding: 0.6em;
|
||||
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||
}
|
||||
}
|
||||
|
|