Merge branch 'develop' into 'kludge/null-status'

# Conflicts:
#   src/services/entity_normalizer/entity_normalizer.service.js
This commit is contained in:
HJ 2024-12-26 23:32:44 +00:00
commit 1c5cfea174
567 changed files with 42526 additions and 14781 deletions

View file

@ -1,5 +1,5 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
"comments": false
"comments": true
}

7
.browserslistrc Normal file
View file

@ -0,0 +1,7 @@
>0.2%
not op_mini all
Safari > 15
Firefox >= 115
Firefox ESR
Android > 4
not dead

View file

@ -1,7 +1,7 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
@ -21,6 +21,7 @@ module.exports = {
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 0
'vue/require-prop-types': 0,
'vue/multi-word-component-names': 0
}
}

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
/build/webpack.prod.conf.js export-subst

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ test/e2e/reports
selenium-debug.log
.idea/
config/local.json
static/emoji.json
logs/

View file

@ -4,11 +4,36 @@
image: node:16
stages:
- check-changelog
- lint
- build
- test
- deploy
# https://git.pleroma.social/help/ci/yaml/workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
check-changelog:
stage: check-changelog
image: alpine
rules:
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^renovate/
when: never
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == 'weblate'
when: never
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
before_script: ''
after_script: ''
cache: {}
script:
- apk add git
- sh ./tools/check-changelog
lint:
stage: lint
script:
@ -18,6 +43,9 @@ lint:
test:
stage: test
tags:
- amd64
- himem
variables:
APT_CACHE_DIR: apt-cache
script:
@ -29,6 +57,9 @@ test:
build:
stage: build
tags:
- amd64
- himem
script:
- yarn
- npm run build

View file

@ -0,0 +1,25 @@
# Environment info
<!-- Everything is optional and where applicable but the more information the better. -->
* Browser, version, OS, platform:
* Instance URL:
* Frontend version (see settings -> about):
* Backend version (see settings -> about):
* Browser extensions (ublock, rikaichamp etc):
* Known instance/user customizations (i.e. pleromafe mods/forks, instance styles etc)
# Bug description & reproduction steps
<!-- Type out here how to reproduce the bug, what goes wrong and what should go right -->
<!-- Screenshots and videos help a lot ;) any observations might also help -->
<!-- Also mention if there any errors in browser's console if relevant -->
# Bug seriousness
<!-- Everything is optional and free-form -->
* How annoying it is:
* How often does it happen:
* How many people does it affect:
* Is there a workaround for it:
/label ~Bug

View file

@ -0,0 +1,11 @@
# Behavior suggestion/Feature request
<!--
Type out what you want to see changed or what feature you want to see added to
PleormaFE. Please also explain how it would benefit users (or admins/moderators)
and what intended usecase is. Any background information (i.e. porting behavior
from other frontends/services, specific situations, personal preferences etc.)
as well as examples would be greatly appreciated.
-->
/label ~suggestion

View file

@ -0,0 +1,7 @@
<!--
please use one of the templates if applicable, otherwise - type out here
in free-form
-->
/label ~needs-triage

View file

@ -0,0 +1,30 @@
<!--
Feel free to submit merge requests that are work-in-progress, but mark them as
Draft: or WIP:.
Merge requests that have Draft or WIP status will not be merged and have less chances
of being reviewed, but you can still ask people to take a look if you need advice.
-->
# Changes
*
*
*
<!-- List what your merge request changes and how -->
<!--
Try to not to break existing behavior, if your changes do break existing behavior
make it configurable to toggle between old behavior and new. Which one should be
default is up to discussion.
-->
<!-- If your merge request resolves some issue link it like so: "Closes #99999" -->
<!--
If merge request adds some new feature that depends on backend:
1. Make sure it gracefully degrades if backend hasn't been updated to support the feature,
we try to make PleromaFE compatible with older versions of BE so that people can still
update frontend safely without updating backend since it's costly and much riskier.
2. Link related BE merge request here
-->
<!-- Screenshots are welcome -->
/label ~needs-review

View file

@ -1 +1 @@
16.16.0
16.18.1

View file

@ -1,19 +1,41 @@
{
"extends": [
"stylelint-rscss/config",
"stylelint-config-recommended",
"stylelint-config-standard"
"stylelint-config-standard",
"stylelint-config-recommended-scss",
"stylelint-config-html",
"stylelint-config-recommended-vue/scss"
],
"rules": {
"declaration-no-important": true,
"rscss/no-descendant-combinator": false,
"rscss/class-format": [
true,
false,
{
"component": "pascal-case",
"variant": "^-[a-z]\\w+",
"element": "^[a-z]\\w+"
}
],
"selector-class-pattern": null,
"import-notation": null,
"custom-property-pattern": null,
"keyframes-name-pattern": null,
"scss/operator-no-newline-after": null,
"declaration-block-no-redundant-longhand-properties": [
true,
{
"ignoreShorthands": [
"grid-template",
"margin",
"padding",
"border",
"border-width",
"border-style",
"border-color",
"border-radius"
]
}
]
}
}

View file

@ -3,41 +3,203 @@ 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
## 2.7.1
Bugfix release. Added small optimizations to emoji picker that should make it a bit more responsive, however it needs rather large change to make it more performant which might come in a major release.
### Fixed
- AdminFE button no longer scrolls page to top when clicked
- Instance default theme not respected
- Nested panel header having wrong sticky position if navbar height != panel header height
- Toggled buttons having bad contrast (when using v2 theme)
### Changed
- Simplify the OAuth client_name to 'PleromaFE'
- Small optimizations to emoji picker
## 2.7.0
### Known issues
We got some reports related to emoji picker performance, this hopefully will be fixed in 2.7.1.
### Notes
This release overhauls how themes work, themes now need to be "compiled", which can cause some delay when loading for the first time and temporarily look "wrong" in some places (popups, menus, dialogs). Please do report any issues, especially if your theme looks wrong or breaks interface when loading. Also report issues if you're experiencing constant performance issues.
To admins: remember that you can update PleromaFE to recent `master` or `develop` in admin dashboard in "Front-ends" tab, scroll down to find PleromaFE box and click "Reinstall `master`" or dropdown and then "Reinstall `develop`". Currently there is no mechanism to check if there is an update or not.
### Changed
- Overhauled the way themes work, migrating to new Pleroma Interface Style Sheets system aka "Themes 3".
- Notifications are no longer sorted by "seen" status since interacting with them can change their read status and makes UI jumpy. Old behavior can be restored in settings.
- Notifications are now shown through a ServiceWorker (since mobile chrome does not allow them otherwise), it's always enabled, even if previously we only enabled it for WebPush notifications only. If you don't like websites "running" while closed, check how to disable them in your browser. Old way to show notifications will be used as a fallback but might not have all the new features.
- Reorganized Settings modal to move out visual stuff into Appearance tab
### Added
- Emoji pack management to the admin panel
- Support `status` notification type (subscriptions/bell, fixes PleromaFE on newer PleromaBE versions)
- Poll end notifications.
- Added option to not mark all notifications when closing notifications drawer on mobile, this creates a new button to mark all as seen.
- Option to always "show" notifications when using web push for better compatibility with some browsers (chrome, edge, safari)
- Option to toggle what notification types appear in native notifications, by default less important ones (likes, repeats, etc) will no longer show up in native notifications.
- Option to treat non-interactive notifications (likes, repeats et all) as seen for visual purposes (no read mark, ignored in counters, still can show in native notifications)
- Ability to resize UI (and certain components) scale independent of browser/text scale
- Ability to override certain aspects of UI style independent of theme used (UI roundness, fonts, underlay)
- Theme selector with visual previews of the theme
- Display loading and error indicator for conversation page
- Option to only show scrobbles that are recent enough
- Interacting (opening reply box etc) or simply clicking on non-interactive notifications now marks them as read. Clicking on native notifications for non-interactive ones also marks them as seen.
- Support group actors
- Focusing into a tab clears all current desktop notifications
- Ability to change size of emoji
- Ability to view APNG (Animated PNG) attachments.
- Support showing extra notifications in the notifications column
- Create a link to the URL of the scrobble when it's present
- Allow hiding custom emojis in picker.
- Ability to mute sensitive posts (ported from eintei).
- Native notifications now also have "badge" property that matches instance's favicon (visible in Android Chromium at least)
- Display public favorites on user profiles
- Display quotes count on posts and add quotes list page
- Show a dedicated registration notice page when further action is required after registering
### Fixed
- Synchronized requested notification types with backend, hopefully should fix missing notifications for polls and follow requests
- Error that appeared on mobile Chromium (and derivatives) when native notifications are allowed
- Being unable to set notification visibility for reports and follow requests
- Native notifications appearing as many times as there are open tabs. Clicking on notification will focus last focused tab.
- The expiry date indication won't be shown if the poll never expires
- Profile mentions causing a 422 error on newer PleromaBE versions.
- Color inputs are less ugly now
- Unread notifications should now properly catch up between sessions (eventually) in polling mode
- Video posters on Safari
## 2.6.1
### Fixed
- fix admin dashboard not having any feedback on frontend installation
- Fix frontend admin tab crashing when no primary frontend is set
- Add aria attributes to react and extra buttons
## 2.6.0
### Added
- add the initial i18n translation file for Taiwanese (Hokkien), and modify some related files.
- Implemented a very basic instance administration screen
- Implement quoting
### Fixed
- Keep aspect ratio of custom emoji reaction in notification
- Fix openSettingsModalTab so that it correctly opens Settings modal instead of Admin modal
- Add alt text to emoji picker buttons
- Use export-subst gitattribute to allow tarball builds
- fix reports now showing reason/content
- Fix HTML attribute parsing, discard attributes not strating with a letter
- Make MentionsLine aware of line breaking by non-br elements
- Fix a bug where mentioning a user twice will not fill the mention into the textarea
- Fix parsing non-ascii tags
- Fix OAuth2 token lingering after revocation
- fix regex issue in HTML parser/renderer
- don't display quoted status twice
- fix typo in code that prevented cards from showing at all
- Fix react button not working if reaction accounts are not loaded
- Fix react button misalignment on safari ios
- Fix pinned statuses gone when reloading user timeline
- Fix scrolling emoji selector in modal in safari ios
## 2.5.1
### Fixed
- Checkboxes in settings can now work with screenreaders
- Autocomplete in edit boxes can now work with screenreaders
- Status interact buttons now have focus indicator for anonymous users
- Top bar buttons now correctly have text labels
- It is now possible to register if the site admin requires birthday to register
- User cards from search results will correctly popup
- Fix notification attachment icon overflow
- Editing mute words is less laggy
- Repeater's name will no longer mess up with the directionality of the text sitting on the same line
- Unauthenticated access will give better error messages
- It is now easier to close the media viewer with a mouse when there is only one image
- Deleting profile fields can work properly
- Clicking the react button will correctly focus the search box
- Clicking buttons on the top-bar will no longer bring you to the top of the page
- Emoji picker is much faster to load
- `blockquote`s have a better display style
- Announcements posting and editing are now available to everyone with such a privilege, not just admins
- Adding or removing list members will actually work
- Emojis without a pack are now correctly displayed in emoji picker
- Changing notification settings will actually work
### Added
- You can now set and see birthdays
- Optional confirmation dialogs when performing various actions
- You can now set fallback languages
## 2.5.0 - 23.12.2022
### Fixed
- UI no longer lags when switching between mobile and desktop mode
- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything
- Emoji autocomplete popover and picker popover stick to the text cursor.
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
- 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
- "Always show mobile button" is working now
- 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
- Additional HTML sanitization on frontend side in case backend sanitization fails
- Interaction list popovers now properly emojify names
- AdminFE button no longer scrolls page to top when clicked
- User handles with non-ascii domains now have less intrusive indicator for the domain name
- Completely hidden posts still no longer have 1px border
- A lot of accessibility improvements
### Changed
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
- Using Vue 3 now
- A lot of internal dependencies updated
- "(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.
- Settings window has been thoroughly rearranged to make more sense and make navigation 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.
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. (You can expand them to full if need be)
- Slight width/spacing adjustments
- More sizing stuff is font-size dependent now
- Scrollbars are styled/colorized now
- Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in)
- Updated localization files
- Top bar is more useful in mobile mode now.
- "Show new" button is way more compact in mobile mode
- Slightly adjusted placement and spacing of the topbar buttons so it's less easy to accidentally log yourself out
### Added
- 3 column mode: only enables when there's space for it (opt-out, customizable)
- Apologetic pleroma-tan
- New button on timeline header to change some of the new and often-used settings
- Support for lists
- Added ability to edit posts and view post edit history etc.
- Added ability to add personal note to users
- Added initial support for admin announcements
- Added ui for account migration
- Added ui for backups
- Added ability to force-unfollow a user from you
- Emoji are now grouped by pack
- Ability to pin navigation items and collapse the navigation menu
- Ability to rearrange order of attachments when uploading
- Ability to scroll column (or page) to top via panel header button
- 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
- Option to customize what clicking user avatar does in user popover
- Notifications for poll results
- "Favorites" link in navigation
- Very early and somewhat experimental system for automatic settings sync (used only for pinned navigation and apologetic pleroma-tan)
- Implemented remote interaction with statuses for anon visitors
- 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
- Timelines/panels and conversations have sticky headers now (a bit glitchy on some browsers like safari) (opt-out)
## [2.4.2] - 2022-01-09

View file

@ -10,3 +10,5 @@ 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@kazv.moe): Code
- tusooa (tusooa@kazv.moe): Code

View file

@ -1,18 +1,19 @@
# Pleroma-FE
> A single column frontend designed for Pleroma.
> Highly-customizable frontend designed for Pleroma.
![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png)
![screenshot](./image-1.png)
# For Translators
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.
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js).
# FOR ADMINS
Pleroma-FE will set your language by your browser locale, but you can change language in settings.
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
# For instance admins
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. Information of customizing PleromaFE settings/defaults is in our [guide](https://docs-develop.pleroma.social/frontend/CONFIGURATION/) and in case you want to build your own custom version there's [another](https://docs-develop.pleroma.social/frontend/HACKING/)
## Build Setup
# Build Setup
``` bash
# install dependencies
@ -20,13 +21,13 @@ npm install -g yarn
yarn
# serve with hot reload at localhost:8080
npm run dev
yarn dev
# build for production with minification
npm run build
yarn build
# run unit tests
npm run unit
yarn unit
```
# For Contributors:
@ -40,10 +41,4 @@ FE Build process also leaves current commit hash in global variable `___pleromaf
# Configuration
Edit config.json for configuration.
## Options
### 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.
Set configuration settings in AdminFE, additionally you can edit config.json. For more details see [documentation](https://docs-develop.pleroma.social/frontend/CONFIGURATION/).

View file

@ -18,6 +18,9 @@ console.log(
var spinner = ora('building for production...')
spinner.start()
var updateEmoji = require('./update-emoji').updateEmoji
updateEmoji()
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
@ -33,4 +36,8 @@ webpack(webpackConfig, function (err, stats) {
chunks: false,
chunkModules: false
}) + '\n')
if (stats.hasErrors()) {
console.error('See above for errors.')
process.exit(1)
}
})

View file

@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
var updateEmoji = require('./update-emoji').updateEmoji
updateEmoji()
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// Define HTTP proxies to your custom API backend
@ -29,18 +32,20 @@ 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()
})
})
// FIXME: The statement below gives error about hooks being required in webpack 5.
// 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) {
@ -48,7 +53,7 @@ Object.keys(proxyTable).forEach(function (context) {
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(context, options))
app.use(proxyMiddleware.createProxyMiddleware(context, options))
})
// handle fallback for HTML5 history API

27
build/update-emoji.js Normal file
View file

@ -0,0 +1,27 @@
module.exports = {
updateEmoji () {
const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
const fs = require('fs')
Object.keys(emojis)
.map(k => {
emojis[k].map(e => {
delete e.unicode_version
delete e.emoji_version
delete e.skin_tone_support_unicode_version
})
})
const res = {}
Object.keys(emojis)
.map(k => {
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
res[groupId] = emojis[k]
})
console.info('Updating emojis...')
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
console.info('Done.')
}
}

View file

@ -2,9 +2,11 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader')
var ESLintPlugin = require('eslint-webpack-plugin');
var StylelintPlugin = require('stylelint-webpack-plugin');
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -22,7 +24,8 @@ module.exports = {
output: {
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js'
filename: '[name].js',
chunkFilename: '[name].js'
},
optimization: {
splitChunks: {
@ -40,25 +43,15 @@ module.exports = {
'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
},
fallback: {
'querystring': require.resolve('querystring-es3'),
'url': require.resolve('url/')
}
},
module: {
noParse: /node_modules\/localforage\/dist\/localforage.js/,
rules: [
{
enforce: 'pre',
test: /\.(js|vue)$/,
include: projectRoot,
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter'),
sourceMap: config.build.productionSourceMap,
extract: true
}
}
},
{
enforce: 'post',
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
@ -90,22 +83,16 @@ module.exports = {
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
type: 'asset',
generator: {
filename: utils.assetsPath('img/[name].[hash:7][ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
type: 'asset',
generator: {
filename: utils.assetsPath('fonts/[name].[hash:7][ext]')
}
},
{
@ -120,14 +107,18 @@ module.exports = {
entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
}),
new ESLintPlugin({
extensions: ['js', 'vue'],
formatter: require('eslint-formatter-friendly')
}),
new StylelintPlugin({}),
new VueLoaderPlugin(),
// This copies Ruffle's WASM to a directory so that JS side can access it
new CopyPlugin({
patterns: [
{
from: "node_modules/@ruffle-rs/ruffle/*",
to: "static/ruffle",
flatten: true
from: "node_modules/@ruffle-rs/ruffle/**/*",
to: "static/ruffle/[name][ext]"
},
],
options: {

View file

@ -16,7 +16,7 @@ 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,

View file

@ -5,26 +5,38 @@ var webpack = require('webpack')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
let commitHash = require('child_process')
.execSync('git rev-parse --short HEAD')
.toString();
let commitHash = (() => {
const subst = "$Format:%h$";
if(!subst.match(/Format:/)) {
return subst;
} else {
return require('child_process')
.execSync('git rev-parse --short HEAD')
.toString();
}
})();
var webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
devtool: config.build.productionSourceMap ? 'source-map' : false,
optimization: {
minimize: true,
splitChunks: {
chunks: 'all'
}
},
minimizer: [
`...`,
new CssMinimizerPlugin()
]
},
output: {
path: config.build.assetsRoot,
@ -60,9 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
ignoreCustomComments: [/server-generated-meta/]
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}
}),
// split vendor js into its own file
// extract webpack runtime and module manifest to its own file in order to

View file

View file

@ -0,0 +1 @@
Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.

View file

@ -0,0 +1 @@
Support bookmark folders

View file

@ -0,0 +1,9 @@
Updated our build system to support browsers:
Safari >= 15
Firefox >= 115
Android > 4
no Opera Mini support
no IE support
no "dead" (unmaintained) browsers support
This does not guarantee that browsers will or will not work.

1
changelog.d/checkbox.fix Normal file
View file

@ -0,0 +1 @@
checkbox vertical alignment has been fixed

View file

@ -0,0 +1 @@
Fix some of the color manipulation functions

1
changelog.d/custom.add Normal file
View file

@ -0,0 +1 @@
Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree

View file

@ -0,0 +1 @@
Support displaying time in absolute format

View file

@ -0,0 +1 @@
Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes

View file

@ -0,0 +1 @@
fix emoji inconsistencies in notifications, fix some emoji not scaling with interface

View file

@ -0,0 +1 @@
Fix small markup inconsistencies

View file

@ -0,0 +1 @@
Fix whitespaces for multiple status mute reasons, display bot status reason

View file

@ -0,0 +1 @@
Inform users that Smithereen public polls are public

View file

@ -0,0 +1 @@
Simplify the OAuth client_name to 'PleromaFE'

View file

@ -0,0 +1 @@
proper sticky header for conversations on user page

View file

View file

View file

@ -0,0 +1 @@
reply-or-quote buttons now take less space

View file

@ -0,0 +1 @@
Bookmarks visible again on mobile

View file

View file

@ -0,0 +1 @@
Splash screen + loading indicator to make process of identifying initialization issues and load performance

1
changelog.d/tabs.change Normal file
View file

@ -0,0 +1 @@
Tabs now have indentation for better visibility of which tab is currently active

1
changelog.d/themes3.add Normal file
View file

@ -0,0 +1 @@
UI for making v3 themes and palettes, support for bundling v3 themes

View file

@ -0,0 +1 @@
Make UserLink wrappable

View file

@ -0,0 +1 @@
Show only month and day instead of weird "day, hour" format. While at it, fixed typo "defualt" in a comment.

View file

@ -25,7 +25,17 @@ 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](../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.
#### New way (via AdminFE, a bit janky but works)
In backend's [static directory](../backend/configuration/static_dir.md) there should be a folder called `frontends` if you installed any frontends from AdminFE before, otherwise you can create it yourself (ensuring correct permissions). Backend will serve given frontend from path `frontends/{frontend}/{reference}`, where `{frontend}` is name of frontend (`pleroma-fe`) and `{reference}` is version. You could make a production build, move `dist` folder into `frontends/pleroma-fe` and rename it into something like `myCustomVersion`. To actually make backend serve this frontend by default, in AdminFE you'll need to set name/reference in Settings -> Frontend -> Frontends -> Primary.
You could also install from a zip file (i.e. CI build) but AdminFE UI is a bit buggy and lacking, so this approach is not recommended.
Take note that frontend management is in early development and currently there's no way for user to change frontend or version for themselves, primary frontend becomes default frontend for all users and visitors.
#### Old way (replaces everything, hard to maintain, not recommended)
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, and this could remove emojis, other frontends etc. and therefore this approach is not recommended.
### Running production build locally or on a separate server

BIN
image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -3,12 +3,165 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<!-- putting styles here to avoid having to wait for styles to load up -->
<style id="splashscreen">
#splash {
--scale: 1;
width: 100vw;
height: 100vh;
display: grid;
grid-template-rows: auto;
grid-template-columns: auto;
align-content: center;
align-items: center;
justify-content: center;
justify-items: center;
flex-direction: column;
background: #0f161e;
font-family: sans-serif;
color: #b9b9ba;
position: absolute;
z-index: 9999;
font-size: calc(1vw + 1vh + 1vmin);
}
#splash-credit {
position: absolute;
font-size: 14px;
bottom: 16px;
right: 16px;
}
#splash-container {
align-items: center;
}
#mascot-container {
display: flex;
align-items: flex-end;
justify-content: center;
perspective: 60em;
perspective-origin: 0 -15em;
transform-style: preserve-3d;
}
#mascot {
width: calc(10em * var(--scale));
height: calc(10em * var(--scale));
object-fit: contain;
object-position: bottom;
transform: translateZ(-2em);
}
#throbber {
display: grid;
width: calc(5em * 0.5 * var(--scale));
height: calc(8em * 0.5 * var(--scale));
margin-left: 4.1em;
z-index: 2;
grid-template-rows: repeat(8, 1fr);
grid-template-columns: repeat(5, 1fr);
grid-template-areas: "P P . L L"
"P P . L L"
"P P . L L"
"P P . L L"
"P P . . ."
"P P . . ."
"P P . E E"
"P P . E E";
--logoChunkSize: calc(2em * 0.5 * var(--scale))
}
.chunk {
background-color: #e2b188;
box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
}
#chunk-P {
grid-area: P;
border-top-left-radius: calc(var(--logoChunkSize) / 2);
}
#chunk-L {
grid-area: L;
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
}
#chunk-E {
grid-area: E;
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
}
#status {
margin-top: 1em;
line-height: 2;
width: 100%;
text-align: center;
}
#statusError {
display: none;
margin-top: 1em;
font-size: calc(1vw + 1vh + 1vmin);
line-height: 2;
width: 100%;
text-align: center;
}
#statusStack {
display: none;
margin-top: 1em;
font-size: calc((1vw + 1vh + 1vmin) / 2.5);
width: calc(100vw - 5em);
padding: 1em;
text-overflow: ellipsis;
overflow-x: hidden;
text-align: left;
line-height: 2;
}
@media (prefers-reduced-motion) {
#throbber {
animation: none !important;
}
}
</style>
<style id="pleroma-eager-styles" type="text/css"></style>
<style id="pleroma-lazy-styles" type="text/css"></style>
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
</head>
<body class="hidden">
<body style="margin: 0; padding: 0">
<noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="app"></div>
<div id="splash">
<!-- we are hiding entire graphic so no point showing credit -->
<div aria-hidden="true" id="splash-credit">
Art by pipivovott
</div>
<div id="splash-container">
<div aria-hidden="true" id="mascot-container">
<div id="throbber">
<div class="chunk" id="chunk-P">
</div>
<div class="chunk" id="chunk-L">
</div>
<div class="chunk" id="chunk-E">
</div>
</div>
<img id="mascot" src="/static/pleromatan_apology.png">
</div>
<div id="status" class="css-ok">
<!-- (。><) -->
<!-- it's a pseudographic, don't want screenreader read out nonsense -->
<span aria-hidden="true" class="initial-text">(。&gt;&lt;)</span>
</div>
<code id="statusError"></code>
<pre id="statusStack"></pre>
</div>
</div>
<div id="app" class="hidden"></div>
<div id="modal"></div>
<!-- built files will be auto injected -->
<div id="popovers" />
</body>
</html>

View file

@ -1,9 +1,9 @@
{
"name": "pleroma_fe",
"version": "1.0.0",
"description": "A Qvitter-style frontend for certain GS servers.",
"author": "Roger Braun <roger@rogerbraun.net>",
"private": true,
"version": "2.7.1",
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false,
"scripts": {
"dev": "node build/dev-server.js",
"build": "node build/build.js",
@ -11,121 +11,128 @@
"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": "npx stylelint src/components/status/status.scss",
"stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
"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.18.9",
"@babel/runtime": "7.21.5",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/vue-fontawesome": "3.0.1",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/vue-fontawesome": "3.0.3",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@ruffle-rs/ruffle": "^0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.0-alpha.43",
"@vuelidate/validators": "2.0.0-alpha.31",
"body-scroll-lock": "2.7.1",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.8.21",
"@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.4",
"body-scroll-lock": "3.1.5",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12",
"diff": "3.5.0",
"cropperjs": "1.5.13",
"escape-html": "1.0.3",
"js-cookie": "^3.0.1",
"hash-sum": "^2.0.0",
"js-cookie": "3.0.5",
"localforage": "1.10.0",
"parse-link-header": "1.0.1",
"phoenix": "1.6.2",
"punycode.js": "2.1.0",
"qrcode": "1",
"utf8": "^3.0.0",
"vue": "^3.2.31",
"vue-i18n": "9.2.0-beta.40",
"vue-router": "4.1.2",
"vue-template-compiler": "2.6.11",
"vuex": "4.0.2"
"pako": "^2.1.0",
"parse-link-header": "2.0.0",
"phoenix": "1.7.7",
"punycode.js": "2.3.0",
"qrcode": "1.5.3",
"querystring-es3": "0.2.1",
"url": "0.11.0",
"utf8": "3.0.0",
"vue": "3.2.45",
"vue-i18n": "9.2.2",
"vue-router": "4.1.6",
"vue-template-compiler": "2.7.14",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0"
},
"devDependencies": {
"@babel/core": "7.18.9",
"@babel/plugin-transform-runtime": "7.18.9",
"@babel/preset-env": "7.18.9",
"@babel/register": "7.18.9",
"@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-eslint": "7.2.3",
"babel-loader": "8.2.5",
"@babel/core": "7.21.8",
"@babel/eslint-parser": "7.21.8",
"@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5",
"@babel/register": "7.21.0",
"@intlify/vue-i18n-loader": "5.0.1",
"@ungap/event-target": "0.2.4",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.2.2",
"@vue/compiler-sfc": "3.2.45",
"@vue/test-utils": "2.2.8",
"autoprefixer": "10.4.19",
"babel-loader": "9.1.3",
"babel-plugin-lodash": "3.3.4",
"chai": "3.5.0",
"chai": "4.3.7",
"chalk": "1.1.3",
"chromedriver": "87.0.7",
"connect-history-api-fallback": "1.6.0",
"copy-webpack-plugin": "6.4.1",
"cross-spawn": "4.0.2",
"css-loader": "0.28.11",
"chromedriver": "108.0.0",
"connect-history-api-fallback": "2.0.0",
"copy-webpack-plugin": "11.0.0",
"cross-spawn": "7.0.3",
"css-loader": "6.10.0",
"css-minimizer-webpack-plugin": "4.2.2",
"custom-event-polyfill": "1.0.7",
"eslint": "5.16.0",
"eslint-config-standard": "12.0.0",
"eslint-friendly-formatter": "2.0.7",
"eslint-loader": "2.2.1",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-node": "7.0.1",
"eslint-plugin-promise": "4.3.1",
"eslint-plugin-standard": "4.1.0",
"eslint-plugin-vue": "5.2.3",
"eslint": "8.33.0",
"eslint-config-standard": "17.0.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.9.0",
"eslint-webpack-plugin": "3.2.0",
"eventsource-polyfill": "0.9.6",
"express": "4.18.1",
"file-loader": "3.0.1",
"express": "4.18.2",
"function-bind": "1.1.1",
"html-webpack-plugin": "3.2.0",
"http-proxy-middleware": "0.21.0",
"inject-loader": "2.0.1",
"html-webpack-plugin": "5.5.1",
"http-proxy-middleware": "2.0.6",
"iso-639-1": "2.1.15",
"isparta-loader": "2.0.0",
"json-loader": "0.5.7",
"karma": "6.4.0",
"karma-coverage": "1.1.2",
"karma-firefox-launcher": "1.3.0",
"karma": "6.4.4",
"karma-coverage": "2.2.0",
"karma-firefox-launcher": "2.1.3",
"karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "4.0.2",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"lodash": "4.17.21",
"lolex": "1.6.0",
"mini-css-extract-plugin": "0.12.0",
"mocha": "3.5.3",
"nightwatch": "0.9.21",
"opn": "4.0.2",
"mini-css-extract-plugin": "2.7.6",
"mocha": "10.2.0",
"nightwatch": "2.6.25",
"opn": "5.5.0",
"ora": "0.4.1",
"postcss-loader": "3.0.0",
"raw-loader": "0.5.1",
"sass": "1.54.0",
"sass-loader": "7.3.1",
"postcss": "8.4.23",
"postcss-html": "^1.5.0",
"postcss-loader": "7.0.2",
"postcss-scss": "^4.0.6",
"sass": "1.60.0",
"sass-loader": "13.2.2",
"selenium-server": "2.53.1",
"semver": "5.7.1",
"serviceworker-webpack-plugin": "1.0.1",
"semver": "7.3.8",
"serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5",
"sinon": "2.4.1",
"sinon-chai": "2.14.0",
"stylelint": "13.13.1",
"stylelint-config-standard": "20.0.0",
"sinon": "15.0.4",
"sinon-chai": "3.7.0",
"stylelint": "14.16.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended-scss": "^8.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "29.0.0",
"stylelint-rscss": "0.4.0",
"url-loader": "1.1.2",
"vue-loader": "^16.0.0",
"vue-style-loader": "4.1.2",
"webpack": "4.46.0",
"stylelint-webpack-plugin": "^3.3.0",
"vue-loader": "17.0.1",
"vue-style-loader": "4.1.3",
"webpack": "5.75.0",
"webpack-dev-middleware": "3.7.3",
"webpack-hot-middleware": "2.25.1",
"webpack-hot-middleware": "2.25.3",
"webpack-merge": "0.20.0"
},
"engines": {
"node": ">= 4.0.0",
"node": ">= 16.0.0",
"npm": ">= 3.0.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

0
preview.style.js Normal file
View file

View file

@ -4,14 +4,15 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
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 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'
@ -32,24 +33,43 @@ export default {
MobilePostStatusButton,
MobileNav,
DesktopNav,
SettingsModal,
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
UserReportingModal,
PostStatusModal,
EditStatusModal,
StatusHistoryModal,
GlobalNoticeList
},
data: () => ({
mobileActivePanel: 'timeline'
}),
watch: {
themeApplied (value) {
this.removeSplash()
}
},
created () {
// Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
window.addEventListener('resize', this.updateMobileState)
},
mounted () {
if (this.$store.state.interface.themeApplied) {
this.removeSplash()
}
},
unmounted () {
window.removeEventListener('resize', this.updateMobileState)
},
computed: {
themeApplied () {
return this.$store.state.interface.themeApplied
},
layoutModalClass () {
return '-' + this.layoutType
},
classes () {
return [
{
@ -60,6 +80,13 @@ export default {
'-' + this.layoutType
]
},
navClasses () {
const { navbarColumnStretch } = this.$store.getters.mergedConfig
return [
'-' + this.layoutType,
...(navbarColumnStretch ? ['-column-stretch'] : [])
]
},
currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image },
instanceBackground () {
@ -85,11 +112,16 @@ export default {
isChats () {
return this.$route.name === 'chat' || this.$route.name === 'chats'
},
isListEdit () {
return this.$route.name === 'lists-edit'
},
newPostButtonShown () {
if (this.isChats) return false
if (this.isListEdit) return false
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
shoutboxPosition () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
},
@ -114,6 +146,15 @@ export default {
updateMobileState () {
this.$store.dispatch('setLayoutWidth', windowWidth())
this.$store.dispatch('setLayoutHeight', windowHeight())
},
removeSplash () {
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
const splashscreenRoot = document.querySelector('#splash')
splashscreenRoot.addEventListener('transitionend', () => {
splashscreenRoot.remove()
})
splashscreenRoot.classList.add('hidden')
document.querySelector('#app').classList.remove('hidden')
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
<template>
<div
v-show="$store.state.interface.themeApplied"
id="app-loaded"
:style="bgStyle"
>
@ -8,15 +9,22 @@
class="app-bg-wrapper"
/>
<MobileNav v-if="layoutType === 'mobile'" />
<DesktopNav v-else />
<DesktopNav
v-else
:class="navClasses"
/>
<Notifications v-if="currentUser" />
<div
id="content"
class="app-layout container"
:class="classes"
>
<div class="underlay"/>
<div id="sidebar" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }">
<div class="underlay" />
<div
id="sidebar"
class="column -scrollable"
:class="{ '-show-scrollbar': showScrollbars }"
>
<user-panel />
<template v-if="layoutType !== 'mobile'">
<nav-panel />
@ -26,7 +34,11 @@
<div id="notifs-sidebar" />
</template>
</div>
<div id="main-scroller" class="column main" :class="{ '-full-height': isChats }">
<main
id="main-scroller"
class="column main"
:class="{ '-full-height': isChats || isListEdit }"
>
<div
v-if="!currentUser"
class="login-hint panel panel-default"
@ -39,10 +51,14 @@
</router-link>
</div>
<router-view />
</div>
<div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
</main>
<div
id="notifs-column"
class="column -scrollable"
:class="{ '-show-scrollbar': showScrollbars }"
/>
</div>
<media-modal />
<MediaModal />
<shout-panel
v-if="currentUser && shout && !hideShoutbox"
:floating="true"
@ -52,8 +68,10 @@
<MobilePostStatusButton />
<UserReportingModal />
<PostStatusModal />
<SettingsModal />
<div id="modal" />
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal :class="layoutModalClass" />
<UpdateNotification />
<GlobalNoticeList />
</div>
</template>

18
src/_mixins.scss Normal file
View file

@ -0,0 +1,18 @@
@mixin unfocused-style {
@content;
&:focus:not(:focus-visible, :hover) {
@content;
}
}
@mixin focused-style {
&:hover,
&:focus {
@content;
}
&:focus-visible {
@content;
}
}

View file

@ -1,34 +0,0 @@
$main-color: #f58d2c;
$main-background: white;
$darkened-background: whitesmoke;
$fallback--bg: #121a24;
$fallback--fg: #182230;
$fallback--faint: rgba(185, 185, 186, .5);
$fallback--text: #b9b9ba;
$fallback--link: #d8a070;
$fallback--icon: #666;
$fallback--lightBg: rgb(21, 30, 42);
$fallback--lightText: #b9b9ba;
$fallback--border: #222;
$fallback--cRed: #ff0000;
$fallback--cBlue: #0095ff;
$fallback--cGreen: #0fa00f;
$fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5);
$fallback--alertWarning: rgba(111,111,20,.5);
$fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px;
$fallback--btnRadius: 4px;
$fallback--inputRadius: 4px;
$fallback--tooltipRadius: 5px;
$fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px;
$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;

View file

@ -0,0 +1 @@
../../static/pleromatan_apology.png

View file

@ -0,0 +1 @@
../../static/pleromatan_apology_fox.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,6 +1,8 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
@ -11,9 +13,9 @@ import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
import { applyConfig } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
let staticInitialResults = null
@ -58,6 +60,8 @@ const getInstanceConfig = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 })
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@ -118,6 +122,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
copyInstanceOption('theme')
copyInstanceOption('style')
copyInstanceOption('palette')
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
@ -155,8 +162,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
copyInstanceOption('sidebarRight')
return store.dispatch('setTheme', config['theme'])
}
const getTOS = async ({ store }) => {
@ -197,7 +202,7 @@ const getStickers = async ({ store }) => {
const stickers = (await Promise.all(
Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json')
var meta = {}
let meta = {}
if (resPack.ok) {
meta = await resPack.json()
}
@ -238,7 +243,7 @@ const resolveStaffAccounts = ({ store, accounts }) => {
const getNodeInfo = async ({ store }) => {
try {
const res = await preloadFetch('/nodeinfo/2.0.json')
const res = await preloadFetch('/nodeinfo/2.1.json')
if (res.ok) {
const data = await res.json()
const metadata = data.metadata
@ -249,10 +254,15 @@ 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') })
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
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: 'quotingAvailable', value: features.includes('quote_posting') })
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -270,6 +280,7 @@ const getNodeInfo = async ({ store }) => {
const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
const priv = metadata.private
@ -319,16 +330,10 @@ const setConfig = async ({ store }) => {
}
const checkOAuthToken = async ({ store }) => {
return new Promise(async (resolve, reject) => {
if (store.getters.getUserToken()) {
try {
await store.dispatch('loginUser', store.getters.getUserToken())
} catch (e) {
console.error(e)
}
}
resolve()
})
if (store.getters.getUserToken()) {
return store.dispatch('loginUser', store.getters.getUserToken())
}
return Promise.resolve()
}
const afterStoreSetup = async ({ store, i18n }) => {
@ -336,29 +341,24 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('setLayoutHeight', windowHeight())
FaviconService.initFaviconService()
initServiceWorker(store)
window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store })
const { customTheme, customThemeSource } = store.state.config
const { theme } = store.state.instance
const customThemePresent = customThemeSource || customTheme
if (customThemePresent) {
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
applyTheme(customThemeSource)
} else {
applyTheme(customTheme)
}
} else if (theme) {
// do nothing, it will load asynchronously
} else {
console.error('Failed to load any theme!')
try {
await store.dispatch('applyTheme').catch((e) => { console.error('Error setting theme', e) })
} catch (e) {
window.splashError(e)
return Promise.reject(e)
}
applyConfig(store.state.config, i18n.global)
// Now we can try getting the server settings and logging in
// Most of these are preloaded into the index.html so blocking is minimized
await Promise.all([
@ -366,10 +366,11 @@ const afterStoreSetup = async ({ store, i18n }) => {
getInstancePanel({ store }),
getNodeInfo({ store }),
getInstanceConfig({ store })
])
]).catch(e => Promise.reject(e))
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
getTOS({ store })
getStickers({ store })
@ -390,14 +391,24 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.use(store)
app.use(i18n)
// Little thing to get out of invalid theme state
window.resetThemes = () => {
store.dispatch('resetThemeV3')
store.dispatch('resetThemeV3Palette')
store.dispatch('resetThemeV2')
}
app.use(vClickOutside)
app.use(VBodyScrollLock)
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)
app.mount('#app')
// remove after vue 3.3
app.config.unwrapInjectedRef = true
app.mount('#app')
return app
}

View file

@ -20,6 +20,14 @@ 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 ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
import ListsEdit from 'components/lists_edit/lists_edit.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => {
@ -31,7 +39,8 @@ export default (store) => {
}
let routes = [
{ name: 'root',
{
name: 'root',
path: '/',
redirect: _to => {
return (store.state.users.currentUser
@ -45,17 +54,20 @@ export default (store) => {
{ 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',
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
{
name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'remote-user-profile',
{
name: 'remote-user-profile',
path: '/remote-users/:hostname/:username',
component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ 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 },
@ -69,7 +81,18 @@ export default (store) => {
{ 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: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
{ name: 'lists', path: '/lists', component: Lists },
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute },
{ name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders },
{ name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit },
{ name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline },
{ name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit }
]
if (store.state.instance.pleromaChatMessagesAvailable) {

View file

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

View file

@ -1,6 +1,8 @@
import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
@ -15,13 +17,30 @@ const AccountActions = {
'user', 'relationship'
],
data () {
return { }
return {
showingConfirmBlock: false,
showingConfirmRemoveFollower: false
}
},
components: {
ProgressButton,
Popover
Popover,
UserListMenu,
ConfirmModal
},
methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
@ -29,11 +48,30 @@ 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 () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
},
reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
@ -45,6 +83,12 @@ const AccountActions = {
}
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})

View file

@ -6,19 +6,19 @@
:bound-to="{ x: 'container' }"
remove-padding
>
<template v-slot:content>
<template #content>
<div class="dropdown-menu">
<template v-if="relationship.following">
<button
v-if="relationship.showing_reblogs"
class="btn button-default dropdown-item"
class="dropdown-item menu-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!relationship.showing_reblogs"
class="btn button-default dropdown-item"
class="dropdown-item menu-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
@ -28,36 +28,44 @@
class="dropdown-divider"
/>
</template>
<UserListMenu :user="user" />
<button
v-if="relationship.followed_by"
class="dropdown-item menu-item"
@click="removeUserFromFollowers"
>
{{ $t('user_card.remove_follower') }}
</button>
<button
v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
<button
v-if="pleromaChatMessagesAvailable"
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="openChat"
>
{{ $t('user_card.message') }}
</button>
</div>
</template>
<template v-slot:trigger>
<template #trigger>
<button class="button-unstyled ellipsis-button">
<FAIcon
class="icon"
@ -66,24 +74,62 @@
</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"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.AccountActions {
.ellipsis-button {
width: 2.5em;
margin: -0.5em 0;
padding: 0.5em 0;
text-align: center;
&:not(:hover) .icon {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
}
</style>

View file

@ -0,0 +1,57 @@
export default {
name: 'Alert',
selector: '.alert',
validInnerComponents: [
'Text',
'Icon',
'Link',
'Border',
'ButtonUnstyled'
],
variants: {
normal: '.neutral',
error: '.error',
warning: '.warning',
success: '.success'
},
editor: {
border: 1,
aspect: '3 / 1'
},
defaultRules: [
{
directives: {
background: '--text',
opacity: 0.5,
blur: '9px'
}
},
{
parent: {
component: 'Alert'
},
component: 'Border',
directives: {
textColor: '--parent'
}
},
{
variant: 'error',
directives: {
background: '--cRed'
}
},
{
variant: 'warning',
directives: {
background: '--cOrange'
}
},
{
variant: 'success',
directives: {
background: '--cGreen'
}
}
]
}

View file

@ -0,0 +1,108 @@
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
}),
canEditAnnouncement () {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
},
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

View file

@ -0,0 +1,134 @@
<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="canEditAnnouncement"
class="btn button-default"
@click="enterEditMode"
>
{{ $t('announcements.edit_action') }}
</button>
<button
v-if="canEditAnnouncement"
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">
.announcement {
border-bottom: 1px solid var(--border);
border-radius: 0;
padding: var(--status-margin);
.heading,
.body {
margin-bottom: var(--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>

View file

@ -0,0 +1,13 @@
import Checkbox from '../checkbox/checkbox.vue'
const AnnouncementEditor = {
components: {
Checkbox
},
props: {
announcement: Object,
disabled: Boolean
}
}
export default AnnouncementEditor

View file

@ -0,0 +1,63 @@
<template>
<div class="announcement-editor">
<textarea
ref="textarea"
v-model="announcement.content"
class="input 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"
class="input"
: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"
class="input"
:type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled"
>
</span>
<span class="announcement-metadata">
<Checkbox
id="announcement-all-day"
v-model="announcement.allDay"
:disabled="disabled"
>
{{ $t('announcements.all_day_prompt') }}
</Checkbox>
</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>

View file

@ -0,0 +1,58 @@
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
},
canPostAnnouncement () {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_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

View file

@ -0,0 +1,78 @@
<template>
<div class="panel panel-default announcements-page">
<div class="panel-heading">
<h1 class="title">
{{ $t('announcements.page_header') }}
</h1>
</div>
<div class="panel-body">
<section
v-if="canPostAnnouncement"
>
<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">
.announcements-page {
.post-form {
padding: var(--status-margin);
.heading,
.body {
margin-bottom: var(--status-margin);
}
.post-button {
min-width: 10em;
}
}
}
</style>

View file

@ -34,9 +34,10 @@ export default {
height: 100%;
align-items: center;
justify-content: center;
.btn {
margin: .5em;
padding: .5em 2em;
margin: 0.5em;
padding: 0.5em 2em;
}
}
</style>

View file

@ -36,6 +36,7 @@ library.add(
const Attachment = {
props: [
'attachment',
'compact',
'description',
'hideDescription',
'nsfw',
@ -71,7 +72,8 @@ const Attachment = {
{
'-loading': this.loading,
'-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined
'-editable': this.edit !== undefined,
'-compact': this.compact
},
'-type-' + this.type,
this.size && '-size-' + this.size,
@ -129,6 +131,9 @@ const Attachment = {
...mapGetters(['mergedConfig'])
},
watch: {
'attachment.description' (newVal) {
this.localDescription = newVal
},
localDescription (newVal) {
this.onEdit(newVal)
}

View file

@ -1,5 +1,3 @@
@import '../../_variables.scss';
.Attachment {
display: inline-flex;
flex-direction: column;
@ -9,10 +7,8 @@
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);
border-radius: var(--roundness);
border-color: var(--border);
.attachment-wrapper {
flex: 1 1 auto;
@ -84,6 +80,13 @@
}
}
.video-container {
border: none;
outline: none;
color: inherit;
background: transparent;
}
.audio-container {
display: flex;
align-items: flex-end;
@ -102,14 +105,13 @@
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);
color: rgb(255 255 255 / 75%);
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
&::before {
margin: 0;
@ -127,23 +129,26 @@
.attachment-button {
padding: 0;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
border-radius: var(--roundness);
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);
}
&.-contain-fit {
img,
canvas {
object-fit: contain;
}
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
}
}
@ -160,8 +165,9 @@
.image {
flex: 1;
img {
border: 0px;
border: 0;
border-radius: 5px;
height: 100%;
object-fit: cover;
@ -172,9 +178,10 @@
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 1rem;
margin: 0px;
margin: 0;
}
}
}
@ -202,8 +209,7 @@
&.-placeholder {
display: inline-block;
color: $fallback--link;
color: var(--postLink, $fallback--link);
color: var(--link);
overflow: hidden;
white-space: nowrap;
height: auto;
@ -252,17 +258,9 @@
cursor: progress;
}
&.-contain-fit {
img,
canvas {
object-fit: contain;
}
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
&.-compact {
.placeholder-container {
padding-bottom: 0.5em;
}
}
}

View file

@ -0,0 +1,25 @@
export default {
name: 'Attachment',
selector: '.Attachment',
notEditable: true,
validInnerComponents: [
'Border',
'ButtonUnstyled',
'Input'
],
defaultRules: [
{
directives: {
roundness: 3
}
},
{
component: 'ButtonUnstyled',
parent: { component: 'Attachment' },
directives: {
background: '#FFFFFF',
opacity: 0.5
}
}
]
}

View file

@ -38,7 +38,7 @@
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
@ -162,10 +162,11 @@
target="_blank"
>
<FAIcon
size="5x"
:size="compact ? '2x' : '5x'"
:icon="placeholderIconClass"
:title="localDescription"
/>
<p>
<p v-if="!compact">
{{ localDescription }}
</p>
</a>
@ -174,7 +175,6 @@
:is="videoTag"
v-if="type === 'video' && !hidden"
class="video-container"
:class="{ 'button-unstyled': 'isModal' }"
:href="attachment.url"
@click.stop.prevent="openModal"
>
@ -252,7 +252,7 @@
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>

View file

@ -1,3 +1,4 @@
<!-- FIXME THIS NEEDS TO BE REFACTORED TO USE POPOVER -->
<template>
<div
v-click-outside="onClickOutside"
@ -6,12 +7,12 @@
<input
v-model="term"
:placeholder="placeholder"
class="autosuggest-input"
class="input autosuggest-input"
@click="onInputClick"
>
<div
v-if="resultsVisible && filtered.length > 0"
class="autosuggest-results"
class="panel autosuggest-results"
>
<slot
v-for="item in filtered"
@ -24,8 +25,6 @@
<script src="./autosuggest.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.autosuggest {
position: relative;
@ -40,18 +39,15 @@
top: 100%;
right: 0;
max-height: 400px;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
background-color: var(--bg);
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
border-color: var(--border);
border-radius: var(--roundness);
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);
box-shadow: var(--shadow);
overflow-y: auto;
z-index: 1;
}

View file

@ -14,11 +14,9 @@
</div>
</template>
<script src="./avatar_list.js" ></script>
<script src="./avatar_list.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.avatars {
display: flex;
margin: 0;
@ -36,8 +34,7 @@
}
.avatar-small {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
border-radius: var(--roundness);
height: 24px;
width: 24px;
}

View file

@ -0,0 +1,30 @@
export default {
name: 'Badge',
selector: '.badge',
validInnerComponents: [
'Text',
'Icon'
],
variants: {
notification: '.-notification'
},
defaultRules: [
{
component: 'Root',
directives: {
'--badgeNotification': 'color | --cRed'
}
},
{
directives: {
background: '--cGreen'
}
},
{
variant: 'notification',
directives: {
background: '--cRed'
}
}
]
}

View file

@ -1,5 +1,6 @@
import UserCard from '../user_card/user_card.vue'
import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -7,20 +8,13 @@ const BasicUserCard = {
props: [
'user'
],
data () {
return {
userExpanded: false
}
},
components: {
UserCard,
UserPopover,
UserAvatar,
RichContent
RichContent,
UserLink
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}

View file

@ -1,24 +1,22 @@
<template>
<div class="basic-user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar
class="avatar"
:user="user"
@click.prevent="toggleUserExpanded"
/>
<router-link
:to="userProfileLink(user)"
@click.prevent
>
<UserPopover
:user-id="user.id"
:overlay-centers="true"
overlay-centers-selector=".avatar"
>
<UserAvatar
class="user-avatar avatar"
:user="user"
@click.prevent
/>
</UserPopover>
</router-link>
<div
v-if="userExpanded"
class="basic-user-card-expanded-content"
>
<UserCard
:user-id="user.id"
:rounded="true"
:bordered="true"
/>
</div>
<div
v-else
class="basic-user-card-collapsed-content"
>
<div
@ -32,12 +30,10 @@
/>
</div>
<div>
<router-link
<user-link
class="basic-user-card-screen-name"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
:user="user"
/>
</div>
<slot />
</div>
@ -51,7 +47,8 @@
display: flex;
flex: 1 0;
margin: 0;
padding: 0.6em 1em;
--emoji-size: 1em;
&-collapsed-content {
margin-left: 0.7em;

View file

@ -37,6 +37,7 @@
.block-card-content-container {
margin-top: 0.5em;
text-align: right;
button {
width: 10em;
}

View file

@ -0,0 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisH
)
const BookmarkFolderCard = {
props: [
'folder',
'allBookmarks'
],
computed: {
firstLetter () {
return this.folder ? this.folder.name[0] : null
}
}
}
export default BookmarkFolderCard

View file

@ -0,0 +1,111 @@
<template>
<div
v-if="allBookmarks"
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmarks' }"
class="bookmark-folder-name"
>
<span class="icon">
<FAIcon
fixed-width
class="fa-scale-110 menu-icon"
icon="bookmark"
/>
</span>{{ $t('nav.all_bookmarks') }}
</router-link>
</div>
<div
v-else
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
class="bookmark-folder-name"
>
<img
v-if="folder.emoji_url"
class="iconEmoji iconEmoji-image"
:src="folder.emoji_url"
:alt="folder.emoji"
:title="folder.emoji"
>
<span
v-else-if="folder.emoji"
class="iconEmoji"
>
<span>
{{ folder.emoji }}
</span>
</span>
<span
v-else-if="firstLetter"
class="icon iconLetter fa-scale-110"
>{{ firstLetter }}</span>{{ folder.name }}
</router-link>
<router-link
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
class="button-folder-edit"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</router-link>
</div>
</template>
<script src="./bookmark_folder_card.js"></script>
<style lang="scss">
.bookmark-folder-card {
display: flex;
align-items: center;
}
a.bookmark-folder-name {
display: flex;
align-items: center;
flex-grow: 1;
.icon,
.iconLetter,
.iconEmoji {
display: inline-block;
height: 2.5rem;
width: 2.5rem;
margin-right: 0.5rem;
}
.icon,
.iconLetter {
font-size: 1.5rem;
line-height: 2.5rem;
text-align: center;
}
.iconEmoji {
text-align: center;
object-fit: contain;
vertical-align: middle;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.bookmark-folder-name,
.button-folder-edit {
margin: 0;
padding: 1em;
color: var(--link);
}
</style>

View file

@ -0,0 +1,80 @@
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
const BookmarkFolderEdit = {
data () {
return {
name: '',
nameDraft: '',
emoji: '',
emojiUrl: null,
emojiDraft: '',
emojiUrlDraft: null,
emojiPickerExpanded: false,
reallyDelete: false
}
},
components: {
EmojiPicker
},
created () {
if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials
apiService.fetchBookmarkFolders({ credentials })
.then((folders) => {
const folder = folders.find(folder => folder.id === this.id)
if (!folder) return
this.nameDraft = this.name = folder.name
this.emojiDraft = this.emoji = folder.emoji
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
})
},
computed: {
id () {
return this.$route.params.id
}
},
methods: {
selectEmoji (event) {
this.emojiDraft = event.insertion
this.emojiUrlDraft = event.insertionUrl
},
showEmojiPicker () {
if (!this.emojiPickerExpanded) {
this.$refs.picker.showPicker()
}
},
onShowPicker () {
this.emojiPickerExpanded = true
},
onClosePicker () {
this.emojiPickerExpanded = false
},
updateFolder () {
this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
},
createFolder () {
this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
.catch((e) => {
this.$store.dispatch('pushGlobalNotice', {
messageKey: 'bookmark_folders.error',
messageArgs: [e.message],
level: 'error'
})
})
},
deleteFolder () {
this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
this.$router.push({ name: 'bookmark-folders' })
}
}
}
export default BookmarkFolderEdit

View file

@ -0,0 +1,200 @@
<template>
<div class="panel-default panel BookmarkFolderEdit">
<div
ref="header"
class="panel-heading folder-edit-heading"
>
<button
class="button-unstyled go-back-button"
@click="$router.back"
>
<FAIcon
size="lg"
icon="chevron-left"
/>
</button>
<h1 class="title">
<i18n-t
v-if="id"
keypath="bookmark_folders.editing_folder"
scope="global"
>
<template #folderName>
{{ name }}
</template>
</i18n-t>
<i18n-t
v-else
keypath="bookmark_folders.creating_folder"
scope="global"
/>
</h1>
</div>
<div class="panel-body">
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.emoji') }}</label>
<button
class="input input-emoji"
:title="$t('bookmark_folder.emoji_pick')"
@click="showEmojiPicker"
>
<img
v-if="emojiUrlDraft"
class="iconEmoji iconEmoji-image"
:src="emojiUrlDraft"
:alt="emojiDraft"
:title="emojiDraft"
>
<span
v-else-if="emojiDraft"
class="iconEmoji"
>
<span>
{{ emojiDraft }}
</span>
</span>
</button>
<EmojiPicker
ref="picker"
class="emoji-picker-panel"
@emoji="selectEmoji"
@show="onShowPicker"
@close="onClosePicker"
/>
</div>
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.name') }}</label>
<input
id="folder-edit-title"
ref="name"
v-model="nameDraft"
class="input"
>
</div>
</div>
<div class="panel-footer">
<span class="spacer" />
<button
v-if="!id"
class="btn button-default footer-button"
@click="createFolder"
>
{{ $t('bookmark_folders.create') }}
</button>
<button
v-else-if="!reallyDelete"
class="btn button-default footer-button"
@click="reallyDelete = true"
>
{{ $t('bookmark_folders.delete') }}
</button>
<template v-else>
{{ $t('bookmark_folders.really_delete') }}
<button
class="btn button-default footer-button"
@click="deleteFolder"
>
{{ $t('general.yes') }}
</button>
<button
class="btn button-default footer-button"
@click="reallyDelete = false"
>
{{ $t('general.no') }}
</button>
</template>
<div
v-if="id && !reallyDelete"
>
<button
class="btn button-default follow-button"
@click="updateFolder"
>
{{ $t('bookmark_folders.update_folder') }}
</button>
</div>
</div>
</div>
</template>
<script src="./bookmark_folder_edit.js"></script>
<style lang="scss">
.BookmarkFolderEdit {
--panel-body-padding: 0.5em;
overflow: hidden;
display: flex;
flex-direction: column;
.folder-edit-heading {
grid-template-columns: auto minmax(50%, 1fr);
}
.panel-body {
display: flex;
gap: 0.5em;
}
.emoji-picker-panel {
position: absolute;
z-index: 20;
margin-top: 2px;
&.hide {
display: none;
}
}
.input-emoji {
height: 2.5em;
width: 2.5em;
padding: 0;
.iconEmoji {
display: inline-block;
text-align: center;
object-fit: contain;
vertical-align: middle;
height: 2.5em;
width: 2.5em;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.input-wrap {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.go-back-button {
text-align: center;
line-height: 1;
height: 100%;
align-self: start;
width: var(--__panel-heading-height-inner);
}
.btn {
margin: 0 0.5em;
}
.panel-footer {
grid-template-columns: minmax(10%, 1fr);
.footer-button {
min-width: 9em;
}
}
}
</style>

View file

@ -0,0 +1,27 @@
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = {
data () {
return {
isNew: false
}
},
components: {
BookmarkFolderCard
},
computed: {
bookmarkFolders () {
return this.$store.state.bookmarkFolders.allFolders
}
},
methods: {
cancelNewFolder () {
this.isNew = false
},
newFolder () {
this.isNew = true
}
}
}
export default BookmarkFolders

View file

@ -0,0 +1,37 @@
<template>
<div class="Bookmark-folders panel panel-default">
<div class="panel-heading">
<h1 class="title">
{{ $t('nav.bookmark_folders') }}
</h1>
<router-link
:to="{ name: 'bookmark-folder-new' }"
class="button-default btn new-folder-button"
>
{{ $t("bookmark_folders.new") }}
</router-link>
</div>
<div class="panel-body">
<BookmarkFolderCard
:all-bookmarks="true"
class="list-item"
/>
<BookmarkFolderCard
v-for="folder in bookmarkFolders.slice().reverse()"
:key="folder"
:folder="folder"
class="list-item"
/>
</div>
</div>
</template>
<script src="./bookmark_folders.js"></script>
<style lang="scss">
.Bookmark-folders {
.new-folder-button {
padding: 0 0.5em;
}
}
</style>

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
export const BookmarkFoldersMenuContent = {
components: {
NavigationEntry
},
computed: {
...mapState({
folders: getBookmarkFolderEntries
})
}
}
export default BookmarkFoldersMenuContent

View file

@ -0,0 +1,19 @@
<template>
<ul>
<NavigationEntry
:item="{
name: 'bookmarks',
routeObject: { name: 'bookmarks' },
label: 'nav.all_bookmarks',
icon: 'bookmark'
}"
/>
<NavigationEntry
v-for="item in folders"
:key="item.id"
:item="item"
/>
</ul>
</template>
<script src="./bookmark_folders_menu_content.js"></script>

View file

@ -1,16 +1,31 @@
import Timeline from '../timeline/timeline.vue'
const Bookmarks = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
created () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
},
components: {
Timeline
},
computed: {
folderId () {
return this.$route.params.id
},
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
},
watch: {
folderId () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
}
},
unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}
}

View file

@ -3,6 +3,7 @@
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="folderId"
/>
</template>

View file

@ -0,0 +1,13 @@
export default {
name: 'Border',
selector: '/*border*/',
virtual: true,
defaultRules: [
{
directives: {
textColor: '$mod(--parent 10)',
textAuto: 'no-auto'
}
}
]
}

View file

@ -0,0 +1,129 @@
export default {
name: 'Button', // Name of the component
selector: '.button-default', // CSS selector/prefix
// outOfTreeSelector: '' // out-of-tree selector is used when other components are laid over it but it's not part of the tree, see Underlay component
// States, system witll calculate ALL possible combinations of those and prepend "normal" to them + standalone "normal" state
states: {
// States are a bit expensive - the amount of combinations generated is about (1/6)n^3+n, so adding more state increased number of combination by an order of magnitude!
// All states inherit from "normal" state, there is no other inheirtance, i.e. hover+disabled only inherits from "normal", not from hover nor disabled.
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
// normal: '' // normal state is implicitly added, it is always included
toggled: '.toggled',
focused: ':focus-visible',
pressed: ':focus:active',
hover: ':hover:not(:disabled)',
disabled: ':disabled'
},
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
variants: {
// Variants save on computation time since adding new variant just adds one more "set".
// normal: '', // you can override normal variant, it will be appenended to the main class
danger: '.danger'
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist.
},
editor: {
aspect: '2 / 1'
},
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
validInnerComponents: [
'Text',
'Icon'
],
// Default rules, used as "default theme", essentially.
defaultRules: [
{
component: 'Root',
directives: {
'--buttonDefaultHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--buttonDefaultFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
'--buttonPressedBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
}
},
{
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
// like within it
directives: {
background: '--fg',
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3
}
},
{
state: ['hover'],
directives: {
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
}
},
{
state: ['focused'],
directives: {
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
}
},
{
state: ['pressed'],
directives: {
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['pressed', 'hover'],
directives: {
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
}
},
{
state: ['toggled'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'hover'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonPressedBevel']
}
},
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel']
}
},
{
component: 'Text',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -0,0 +1,97 @@
export default {
name: 'ButtonUnstyled',
selector: '.button-unstyled',
notEditable: true,
states: {
toggled: '.toggled',
disabled: ':disabled',
hover: ':hover:not(:disabled)',
focused: ':focus-within'
},
validInnerComponents: [
'Text',
'Icon',
'Badge'
],
defaultRules: [
{
directives: {
background: '#ffffff',
opacity: 0,
shadow: []
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused', 'hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Text',
parent: {
component: 'ButtonUnstyled',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -57,6 +57,7 @@ const Chat = {
},
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleResize)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.dispatch('clearCurrentChat')
},
@ -107,7 +108,7 @@ const Chat = {
}
})
},
'$route': function () {
$route: function () {
this.startFetching()
},
mastoUserSocketStatus (newValue) {
@ -135,7 +136,7 @@ const Chat = {
},
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
handleResize (opts = {}) {
const { expand = false, delayed = false } = opts
const { delayed = false } = opts
if (delayed) {
setTimeout(() => {
@ -146,10 +147,10 @@ const Chat = {
this.$nextTick(() => {
const { offsetHeight = undefined } = getScrollPosition()
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
if (diff !== 0 || (!this.bottomedOut() && expand)) {
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
if (diff !== 0 && !this.bottomedOut()) {
this.$nextTick(() => {
window.scrollTo({ top: window.scrollY + diff })
window.scrollBy({ top: -Math.trunc(diff) })
})
}
this.lastScrollPosition = getScrollPosition()
@ -187,6 +188,7 @@ const Chat = {
}, 5000)
},
handleScroll: _.throttle(function () {
this.lastScrollPosition = getScrollPosition()
if (!this.currentChat) { return }
if (this.reachedTop()) {

View file

@ -11,15 +11,15 @@
.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));
margin: 0 0 0 0;
border-radius: 10px 10px 0 0;
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
margin: 0;
border-radius: var(--roundness);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
&::after {
border-radius: 0;
@ -37,8 +37,6 @@
.footer {
position: sticky;
bottom: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
z-index: 1;
}
@ -61,12 +59,10 @@
position: absolute;
right: 1.3em;
top: -3.2em;
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
box-shadow: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 4px rgb(0 0 0 / 30%);
z-index: 10;
transition: 0.35s all;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
@ -79,12 +75,6 @@
visibility: visible;
}
i {
font-size: 1em;
color: $fallback--text;
color: var(--text, $fallback--text);
}
.unread-message-count {
font-size: 0.8em;
left: 50%;

View file

@ -0,0 +1,19 @@
export default {
name: 'Chat',
selector: '.chat-message-list',
validInnerComponents: [
'Text',
'Link',
'Icon',
'Avatar',
'ChatMessage'
],
defaultRules: [
{
directives: {
background: '--bg',
blur: '5px'
}
}
]
}

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