Compare commits

..

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

254 changed files with 5952 additions and 14799 deletions

View file

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

View file

@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project. # This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at: # Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/ # https://hub.docker.com/r/library/node/tags/
image: node:12 image: node:10
stages: stages:
- lint - lint

View file

@ -3,67 +3,11 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## [Unreleased]
### Fixed
- AdminFE button no longer scrolls page to top when clicked
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
- Fixed many many bugs related to new mentions, including spacing and alignment issues
- Links in profile bios now properly open in new tabs
- Inline images now respect their intended width/height attributes
- Links with `&` in them work properly now
- Interaction list popovers now properly emojify names
- Completely hidden posts still had 1px border
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
- Attachment description is prefilled with backend-provided default when uploading
- Proper visual feedback that next image is loading when browsing
### Changed
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
- User highlight background now also covers the `@`
- Reverted back to textual `@`, svg version is opt-in.
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
- Uploaded attachments are uniform with displayed attachments
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
### Added
- Options to show domains in mentions
- Option to show user avatars in mention links (opt-in)
- Option to disable the tooltip for mentions
- Option to completely hide muted threads
- Ability to open videos in modal even if you disabled that feature, via an icon button
- New button on attachment that indicates that attachment has a description and shows a bar filled with description
- Attachments are truncated just like post contents
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
- Ability to rearrange order of attachments when uploading
- Enabled users to zoom and pan images in media viewer with mouse and touch
## [2.4.2] - 2022-01-09
### Added
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
- Implemented user option to always show floating New Post button (normally mobile-only)
- Display reasons for instance specific policies
- Added functionality to cancel follow request
### Fixed
- Fixed link to external profile not working on user profiles
- Fixed mobile shoutbox display
- Fixed favicon badge not working in Chrome
- Escape html more properly in subject/display name
## [2.4.0] - 2021-08-08
### Added ### Added
- Added a quick settings to timeline header for easier access - Added a quick settings to timeline header for easier access
- Added option to mark posts as sensitive by default - Added option to mark posts as sensitive by default
- Added quick filters for notifications - Added quick filters for notifications
- Implemented user option to change sidebar position to the right side
- Implemented user option to hide floating shout panel
- Implemented "edit profile" button if viewing own profile which opens profile settings
### Fixed
- Fixed follow request count showing in the wrong location in mobile view
## [2.3.0] - 2021-03-01 ## [2.3.0] - 2021-03-01

View file

@ -3,7 +3,6 @@ Contributors of this project.
- Constance Variable (lambadalambda@social.heldscal.la): Code - Constance Variable (lambadalambda@social.heldscal.la): Code
- Coco Snuss (cocosnuss@social.heldscal.la): Code - Coco Snuss (cocosnuss@social.heldscal.la): Code
- wakarimasen (wakarimasen@shitposter.club): NSFW hiding image - wakarimasen (wakarimasen@shitposter.club): NSFW hiding image
- eris (eris@disqordia.space): Code
- dtluna (dtluna@social.heldscal.la): Code - dtluna (dtluna@social.heldscal.la): Code
- sonyam (sonyam@social.heldscal.la): Background images - sonyam (sonyam@social.heldscal.la): Background images
- hakui (hakui@freezepeach.xyz): CSS and styling - hakui (hakui@freezepeach.xyz): CSS and styling

View file

@ -21,7 +21,6 @@ var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, { var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath, publicPath: webpackConfig.output.publicPath,
writeToDisk: true,
stats: { stats: {
colors: true, colors: true,
chunks: false chunks: false

View file

@ -3,8 +3,6 @@ var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../') var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the // check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -30,16 +28,16 @@ module.exports = {
} }
}, },
resolve: { resolve: {
extensions: ['.js', '.jsx', '.vue'], extensions: ['.js', '.vue'],
modules: [ modules: [
path.join(__dirname, '../node_modules') path.join(__dirname, '../node_modules')
], ],
alias: { alias: {
'vue$': 'vue/dist/vue.runtime.common',
'static': path.resolve(__dirname, '../static'), 'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'), 'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'), 'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components'), 'components': path.resolve(__dirname, '../src/components')
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
} }
}, },
module: { module: {
@ -59,28 +57,9 @@ module.exports = {
} }
} }
}, },
{
enforce: 'post',
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
type: 'javascript/auto',
loader: '@intlify/vue-i18n-loader',
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
path.resolve(__dirname, '../src/i18n')
]
},
{ {
test: /\.vue$/, test: /\.vue$/,
loader: 'vue-loader', use: 'vue-loader'
options: {
compilerOptions: {
isCustomElement(tag) {
if (tag === 'pinch-zoom') {
return true
}
return false
}
}
}
}, },
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
@ -114,20 +93,6 @@ module.exports = {
new ServiceWorkerWebpackPlugin({ new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '..', 'src/sw.js'), entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js' filename: 'sw-pleroma.js'
}),
new VueLoaderPlugin(),
// This copies Ruffle's WASM to a directory so that JS side can access it
new CopyPlugin({
patterns: [
{
from: "node_modules/ruffle-mirror/*",
to: "static/ruffle",
flatten: true
},
],
options: {
concurrency: 100,
},
}) })
] ]
} }

View file

@ -21,9 +21,7 @@ module.exports = merge(baseWebpackConfig, {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': config.dev.env, 'process.env': config.dev.env,
'COMMIT_HASH': JSON.stringify('DEV'), 'COMMIT_HASH': JSON.stringify('DEV'),
'DEV_OVERRIDES': JSON.stringify(config.dev.settings), 'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false
}), }),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),

View file

@ -36,9 +36,7 @@ var webpackConfig = merge(baseWebpackConfig, {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': env, 'process.env': env,
'COMMIT_HASH': JSON.stringify(commitHash), 'COMMIT_HASH': JSON.stringify(commitHash),
'DEV_OVERRIDES': JSON.stringify(undefined), 'DEV_OVERRIDES': JSON.stringify(undefined)
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false
}), }),
// extract css into its own file // extract css into its own file
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({

View file

@ -52,10 +52,7 @@ module.exports = {
target, target,
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost', cookieDomainRewrite: 'localhost',
ws: true, ws: true
headers: {
'Origin': target
}
}, },
'/oauth/revoke': { '/oauth/revoke': {
target, target,

View file

@ -16,111 +16,105 @@
"lint-fix": "eslint --fix --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": { "dependencies": {
"@babel/runtime": "7.17.8", "@babel/runtime": "^7.7.6",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "3.0.0-5", "@fortawesome/vue-fontawesome": "^2.0.0",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "body-scroll-lock": "^2.6.4",
"@vuelidate/core": "2.0.0-alpha.35", "chromatism": "^3.0.0",
"@vuelidate/validators": "2.0.0-alpha.27", "cropperjs": "^1.4.3",
"body-scroll-lock": "2.7.1", "diff": "^3.0.1",
"chromatism": "3.0.0", "escape-html": "^1.0.3",
"click-outside-vue3": "4.0.1", "localforage": "^1.5.0",
"cropperjs": "1.5.12", "parse-link-header": "^1.0.1",
"diff": "3.5.0", "phoenix": "^1.3.0",
"escape-html": "1.0.3", "portal-vue": "^2.1.4",
"localforage": "1.10.0", "punycode.js": "^2.1.0",
"parse-link-header": "1.0.1", "v-click-outside": "^2.1.1",
"phoenix": "1.6.2", "vue": "^2.6.11",
"punycode.js": "2.1.0", "vue-i18n": "^7.3.2",
"qrcode": "1", "vue-router": "^3.0.1",
"ruffle-mirror": "2021.12.31", "vue-template-compiler": "^2.6.11",
"vue": "^3.2.31", "vuelidate": "^0.7.4",
"vue-i18n": "^9.2.0-beta.34", "vuex": "^3.0.1"
"vue-router": "4.0.14",
"vue-template-compiler": "2.6.11",
"vuex": "4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.17.8", "@babel/core": "^7.7.5",
"@babel/plugin-transform-runtime": "7.17.0", "@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "^7.7.6",
"@babel/register": "7.17.7", "@babel/register": "^7.7.4",
"@intlify/vue-i18n-loader": "^5.0.0", "@ungap/event-target": "^0.1.0",
"@ungap/event-target": "0.2.3", "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1", "@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/babel-plugin-jsx": "1.1.1", "@vue/test-utils": "^1.0.0-beta.26",
"@vue/compiler-sfc": "^3.1.0", "autoprefixer": "^6.4.0",
"@vue/test-utils": "2.0.0-rc.17", "babel-eslint": "^7.0.0",
"autoprefixer": "6.7.7", "babel-loader": "^8.0.6",
"babel-eslint": "7.2.3", "babel-plugin-lodash": "^3.3.4",
"babel-loader": "8.2.4", "chai": "^3.5.0",
"babel-plugin-lodash": "3.3.4", "chalk": "^1.1.3",
"chai": "3.5.0", "chromedriver": "^87.0.1",
"chalk": "1.1.3", "connect-history-api-fallback": "^1.1.0",
"chromedriver": "87.0.7", "cross-spawn": "^4.0.2",
"connect-history-api-fallback": "1.6.0", "css-loader": "^0.28.0",
"copy-webpack-plugin": "6.4.1", "custom-event-polyfill": "^1.0.7",
"cross-spawn": "4.0.2", "eslint": "^5.16.0",
"css-loader": "0.28.11", "eslint-config-standard": "^12.0.0",
"custom-event-polyfill": "1.0.7", "eslint-friendly-formatter": "^2.0.5",
"eslint": "5.16.0", "eslint-loader": "^2.1.0",
"eslint-config-standard": "12.0.0", "eslint-plugin-import": "^2.13.0",
"eslint-friendly-formatter": "2.0.7", "eslint-plugin-node": "^7.0.0",
"eslint-loader": "2.2.1", "eslint-plugin-promise": "^4.0.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-node": "7.0.1", "eslint-plugin-vue": "^5.2.2",
"eslint-plugin-promise": "4.3.1", "eventsource-polyfill": "^0.9.6",
"eslint-plugin-standard": "4.1.0", "express": "^4.13.3",
"eslint-plugin-vue": "5.2.3", "file-loader": "^3.0.1",
"eventsource-polyfill": "0.9.6", "function-bind": "^1.0.2",
"express": "4.17.3", "html-webpack-plugin": "^3.0.0",
"file-loader": "3.0.1", "http-proxy-middleware": "^0.17.2",
"function-bind": "1.1.1", "inject-loader": "^2.0.1",
"html-webpack-plugin": "3.2.0", "iso-639-1": "^2.0.3",
"http-proxy-middleware": "0.21.0", "isparta-loader": "^2.0.0",
"inject-loader": "2.0.1", "json-loader": "^0.5.4",
"iso-639-1": "2.1.13", "karma": "^3.0.0",
"isparta-loader": "2.0.0", "karma-coverage": "^1.1.1",
"json-loader": "0.5.7", "karma-firefox-launcher": "^1.1.0",
"karma": "6.3.17", "karma-mocha": "^1.2.0",
"karma-coverage": "1.1.2", "karma-mocha-reporter": "^2.2.1",
"karma-firefox-launcher": "1.3.0", "karma-sinon-chai": "^2.0.2",
"karma-mocha": "2.0.1", "karma-sourcemap-loader": "^0.3.7",
"karma-mocha-reporter": "2.2.5", "karma-spec-reporter": "0.0.26",
"karma-sinon-chai": "2.0.2", "karma-webpack": "^4.0.0-rc.3",
"karma-sourcemap-loader": "0.3.8", "lodash": "^4.16.4",
"karma-spec-reporter": "0.0.33", "lolex": "^1.4.0",
"karma-webpack": "4.0.2", "mini-css-extract-plugin": "^0.5.0",
"lodash": "4.17.21", "mocha": "^3.1.0",
"lolex": "1.6.0", "nightwatch": "^0.9.8",
"mini-css-extract-plugin": "0.12.0", "opn": "^4.0.2",
"mocha": "3.5.3", "ora": "^0.3.0",
"nightwatch": "0.9.21", "postcss-loader": "^3.0.0",
"opn": "4.0.2", "raw-loader": "^0.5.1",
"ora": "0.4.1", "sass": "^1.17.3",
"postcss-loader": "3.0.0", "sass-loader": "git://github.com/webpack-contrib/sass-loader",
"raw-loader": "0.5.1",
"sass": "1.20.1",
"sass-loader": "7.2.0",
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "5.6.0", "semver": "^5.3.0",
"serviceworker-webpack-plugin": "1.0.1", "serviceworker-webpack-plugin": "^1.0.0",
"shelljs": "0.8.5", "shelljs": "^0.8.4",
"sinon": "2.4.1", "sinon": "^2.1.0",
"sinon-chai": "2.14.0", "sinon-chai": "^2.8.0",
"stylelint": "13.6.1", "stylelint": "^13.6.1",
"stylelint-config-standard": "20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-rscss": "0.4.0", "stylelint-rscss": "^0.4.0",
"url-loader": "1.1.2", "url-loader": "^1.1.2",
"vue-loader": "^16.0.0", "vue-loader": "^14.0.0",
"vue-style-loader": "4.1.2", "vue-style-loader": "^4.0.0",
"webpack": "4.46.0", "webpack": "^4.0.0",
"webpack-dev-middleware": "3.7.3", "webpack-dev-middleware": "^3.6.0",
"webpack-hot-middleware": "2.24.3", "webpack-hot-middleware": "^2.12.2",
"webpack-merge": "0.14.1" "webpack-merge": "^0.14.1"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 4.0.0",

View file

@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}

View file

@ -4,7 +4,7 @@ import Notifications from './components/notifications/notifications.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ShoutPanel from './components/shout_panel/shout_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue' import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue' import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
@ -26,7 +26,7 @@ export default {
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
ShoutPanel, ChatPanel,
MediaModal, MediaModal,
SideDrawer, SideDrawer,
MobilePostStatusButton, MobilePostStatusButton,
@ -46,7 +46,7 @@ export default {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
window.addEventListener('resize', this.updateMobileState) window.addEventListener('resize', this.updateMobileState)
}, },
unmounted () { destroyed () {
window.removeEventListener('resize', this.updateMobileState) window.removeEventListener('resize', this.updateMobileState)
}, },
computed: { computed: {
@ -65,7 +65,7 @@ export default {
} }
} }
}, },
shout () { return this.$store.state.shout.joined }, chat () { return this.$store.state.chat.channel.state === 'joined' },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () { showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel && return this.$store.state.instance.showInstanceSpecificPanel &&
@ -73,17 +73,11 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
shoutboxPosition () {
return this.$store.getters.mergedConfig.showNewPostButton || false
},
hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox
},
isMobileLayout () { return this.$store.state.interface.mobileLayout }, isMobileLayout () { return this.$store.state.interface.mobileLayout },
privateMode () { return this.$store.state.instance.private }, privateMode () { return this.$store.state.instance.private },
sidebarAlign () { sidebarAlign () {
return { return {
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 'order': this.$store.state.instance.sidebarRight ? 99 : 0
} }
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])

View file

@ -88,10 +88,6 @@ a {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
&.-sublime {
background: transparent;
}
i[class*=icon-], i[class*=icon-],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: $fallback--text;
@ -191,7 +187,7 @@ a {
} }
} }
input, textarea, .input { input, textarea, .select, .input {
&.unstyled { &.unstyled {
border-radius: 0; border-radius: 0;
@ -221,11 +217,47 @@ input, textarea, .input {
hyphens: none; hyphens: none;
padding: 8px .5em; padding: 8px .5em;
&:disabled, &[disabled=disabled], &.disabled { &.select {
padding: 0;
}
&:disabled, &[disabled=disabled] {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
.select-down-icon {
position: absolute;
top: 0;
bottom: 0;
right: 5px;
height: 100%;
color: $fallback--text;
color: var(--inputText, $fallback--text);
line-height: 28px;
z-index: 0;
pointer-events: none;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: transparent;
border: none;
color: $fallback--text;
color: var(--inputText, --text, $fallback--text);
margin: 0;
padding: 0 2em 0 .2em;
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-size: 14px;
width: 100%;
z-index: 1;
height: 28px;
line-height: 16px;
}
&[type=range] { &[type=range] {
background: none; background: none;
border: none; border: none;
@ -572,7 +604,7 @@ nav {
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
transition: opacity .2s transition: opacity .2s
} }
.fade-enter-from, .fade-leave-active { .fade-enter, .fade-leave-active {
opacity: 0 opacity: 0
} }
@ -798,6 +830,13 @@ nav {
} }
} }
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: .5em;
}
}
.setting-list, .setting-list,
.option-list{ .option-list{
list-style-type: none; list-style-type: none;

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
id="app-loaded" id="app"
:style="bgStyle" :style="bgStyle"
> >
<div <div
@ -49,17 +49,16 @@
</div> </div>
<media-modal /> <media-modal />
</div> </div>
<shout-panel <chat-panel
v-if="currentUser && shout && !hideShoutbox" v-if="currentUser && chat"
:floating="true" :floating="true"
class="floating-shout mobile-hidden" class="floating-chat mobile-hidden"
:class="{ 'left': shoutboxPosition }"
/> />
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
<PostStatusModal /> <PostStatusModal />
<SettingsModal /> <SettingsModal />
<div id="modal" /> <portal-target name="modal" />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View file

@ -30,5 +30,3 @@ $fallback--attachmentRadius: 10px;
$fallback--chatMessageRadius: 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; $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

@ -1,13 +1,7 @@
import { createApp } from 'vue' import Vue from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import VueRouter from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import App from '../App.vue'
import routes from './routes' import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils' import { windowWidth } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
@ -121,7 +115,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('nsfwCensorImage') copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background') copyInstanceOption('background')
copyInstanceOption('hidePostStats') copyInstanceOption('hidePostStats')
copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats') copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses') copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo') copyInstanceOption('logo')
@ -247,7 +240,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
@ -373,32 +366,25 @@ const afterStoreSetup = async ({ store, i18n }) => {
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
const router = createRouter({ const router = new VueRouter({
history: createWebHistory(), mode: 'history',
routes: routes(store), routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) { if (to.matched.some(m => m.meta.dontScroll)) {
return false return false
} }
return savedPosition || { left: 0, top: 0 } return savedPosition || { x: 0, y: 0 }
} }
}) })
const app = createApp(App) /* eslint-disable no-new */
return new Vue({
app.use(router) router,
app.use(store) store,
app.use(i18n) i18n,
el: '#app',
app.use(vClickOutside) render: h => h(App)
app.use(VBodyScrollLock) })
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)
app.mount('#app')
return app
} }
export default afterStoreSetup export default afterStoreSetup

View file

@ -16,7 +16,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import Notifications from 'components/notifications/notifications.vue' import Notifications from 'components/notifications/notifications.vue'
import AuthForm from 'components/auth_form/auth_form.js' import AuthForm from 'components/auth_form/auth_form.js'
import ShoutPanel from 'components/shout_panel/shout_panel.vue' import ChatPanel from 'components/chat_panel/chat_panel.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue' import About from 'components/about/about.vue'
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
@ -46,7 +46,7 @@ export default (store) => {
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'remote-user-profile-acct', { name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
@ -64,12 +64,12 @@ export default (store) => {
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile } { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
] ]
if (store.state.instance.pleromaChatMessagesAvailable) { if (store.state.instance.pleromaChatMessagesAvailable) {

View file

@ -6,7 +6,10 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template v-slot:content> <div
slot="content"
class="account-tools-popover"
>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <button
@ -56,15 +59,16 @@
{{ $t('user_card.message') }} {{ $t('user_card.message') }}
</button> </button>
</div> </div>
</template> </div>
<template v-slot:trigger> <div
<button class="button-unstyled ellipsis-button"> slot="trigger"
<FAIcon class="ellipsis-button"
class="icon" >
icon="ellipsis-v" <FAIcon
/> class="icon"
</button> icon="ellipsis-v"
</template> />
</div>
</Popover> </Popover>
</div> </div>
</template> </template>
@ -79,6 +83,7 @@
} }
.ellipsis-button { .ellipsis-button {
cursor: pointer;
width: 2.5em; width: 2.5em;
margin: -0.5em 0; margin: -0.5em 0;
padding: 0.5em 0; padding: 0.5em 0;

View file

@ -19,7 +19,6 @@
<script> <script>
export default { export default {
emits: ['resetAsyncComponent'],
methods: { methods: {
retry () { retry () {
this.$emit('resetAsyncComponent') this.$emit('resetAsyncComponent')

View file

@ -1,5 +1,4 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import Flash from '../flash/flash.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png' import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
@ -11,12 +10,7 @@ import {
faImage, faImage,
faVideo, faVideo,
faPlayCircle, faPlayCircle,
faTimes, faTimes
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt,
faAlignRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -25,64 +19,36 @@ library.add(
faImage, faImage,
faVideo, faVideo,
faPlayCircle, faPlayCircle,
faTimes, faTimes
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt,
faAlignRight
) )
const Attachment = { const Attachment = {
props: [ props: [
'attachment', 'attachment',
'description',
'hideDescription',
'nsfw', 'nsfw',
'size', 'size',
'allowPlay',
'setMedia', 'setMedia',
'remove', 'naturalSizeLoad'
'shiftUp',
'shiftDn',
'edit'
], ],
data () { data () {
return { return {
localDescription: this.description || this.attachment.description,
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage, preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false, loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false
flashLoaded: false,
showDescription: false
} }
}, },
components: { components: {
Flash,
StillImage, StillImage,
VideoAttachment VideoAttachment
}, },
computed: { computed: {
classNames () {
return [
{
'-loading': this.loading,
'-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined
},
'-type-' + this.type,
this.size && '-size-' + this.size,
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
]
},
usePlaceholder () { usePlaceholder () {
return this.size === 'hide' return this.size === 'hide' || this.type === 'unknown'
},
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
}, },
placeholderName () { placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) { if (this.attachment.description === '' || !this.attachment.description) {
@ -106,33 +72,24 @@ const Attachment = {
return this.nsfw && this.hideNsfwLocal && !this.showHidden return this.nsfw && this.hideNsfwLocal && !this.showHidden
}, },
isEmpty () { isEmpty () {
return (this.type === 'html' && !this.attachment.oembed) return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
},
isSmall () {
return this.size === 'small'
},
fullwidth () {
if (this.size === 'hide') return false
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
}, },
useModal () { useModal () {
let modalTypes = [] const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
switch (this.size) { : this.mergedConfig.playVideosInModal
case 'hide': ? ['image', 'video']
case 'small': : ['image']
modalTypes = ['image', 'video', 'audio', 'flash']
break
default:
modalTypes = this.mergedConfig.playVideosInModal
? ['image', 'video', 'flash']
: ['image']
break
}
return modalTypes.includes(this.type) return modalTypes.includes(this.type)
}, },
videoTag () {
return this.useModal ? 'button' : 'span'
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: {
localDescription (newVal) {
this.onEdit(newVal)
}
},
methods: { methods: {
linkClicked ({ target }) { linkClicked ({ target }) {
if (target.tagName === 'A') { if (target.tagName === 'A') {
@ -141,37 +98,12 @@ const Attachment = {
}, },
openModal (event) { openModal (event) {
if (this.useModal) { if (this.useModal) {
this.$emit('setMedia') event.stopPropagation()
this.$store.dispatch('setCurrentMedia', this.attachment) event.preventDefault()
} else if (this.type === 'unknown') { this.setMedia()
window.open(this.attachment.url) this.$store.dispatch('setCurrent', this.attachment)
} }
}, },
openModalForce (event) {
this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment)
},
onEdit (event) {
this.edit && this.edit(this.attachment, event)
},
onRemove () {
this.remove && this.remove(this.attachment)
},
onShiftUp () {
this.shiftUp && this.shiftUp(this.attachment)
},
onShiftDn () {
this.shiftDn && this.shiftDn(this.attachment)
},
stopFlash () {
this.$refs.flash.closePlayer()
},
setFlashLoaded (event) {
this.flashLoaded = event
},
toggleDescription () {
this.showDescription = !this.showDescription
},
toggleHidden (event) { toggleHidden (event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
@ -198,7 +130,7 @@ const Attachment = {
onImageLoad (image) { onImageLoad (image) {
const width = image.naturalWidth const width = image.naturalWidth
const height = image.naturalHeight const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
} }
} }
} }

View file

@ -1,268 +0,0 @@
@import '../../_variables.scss';
.Attachment {
display: inline-flex;
flex-direction: column;
position: relative;
align-self: flex-start;
line-height: 0;
height: 100%;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
.attachment-wrapper {
flex: 1 1 auto;
height: 100%;
position: relative;
overflow: hidden;
}
.description-container {
flex: 0 1 0;
display: flex;
padding-top: 0.5em;
z-index: 1;
p {
flex: 1;
text-align: center;
line-height: 1.5;
padding: 0.5em;
margin: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&.-static {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding-top: 0;
background: var(--popover);
box-shadow: var(--popupShadow);
}
}
.description-field {
flex: 1;
min-width: 0;
}
& .placeholder-container,
& .image-container,
& .audio-container,
& .video-container,
& .flash-container,
& .oembed-container {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.image-container {
.image {
width: 100%;
height: 100%;
}
}
& .flash-container,
& .video-container {
& .flash,
& video {
width: 100%;
height: 100%;
object-fit: contain;
align-self: center;
}
}
.audio-container {
display: flex;
align-items: flex-end;
audio {
width: 100%;
height: 100%;
}
}
.placeholder-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 0.5em;
}
.play-icon {
position: absolute;
font-size: 64px;
top: calc(50% - 32px);
left: calc(50% - 32px);
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
&::before {
margin: 0;
}
}
.attachment-buttons {
display: flex;
position: absolute;
right: 0;
top: 0;
margin-top: 0.5em;
margin-right: 0.5em;
z-index: 1;
.attachment-button {
padding: 0;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
text-align: center;
width: 2em;
height: 2em;
margin-left: 0.5em;
font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color
background: rgba(230, 230, 230, 0.7);
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
}
}
.oembed-container {
line-height: 1.2em;
flex: 1 0 100%;
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: 5px;
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
&.-size-small {
.play-icon {
zoom: 0.5;
opacity: 0.7;
}
.attachment-buttons {
zoom: 0.7;
opacity: 0.5;
}
}
&.-editable {
padding: 0.5em;
& .description-container,
& .attachment-buttons {
margin: 0;
}
}
&.-placeholder {
display: inline-block;
color: $fallback--link;
color: var(--postLink, $fallback--link);
overflow: hidden;
white-space: nowrap;
height: auto;
line-height: 1.5;
&:not(.-editable) {
border: none;
}
&.-editable {
display: flex;
flex-direction: row;
align-items: baseline;
& .description-container,
& .attachment-buttons {
margin: 0;
padding: 0;
position: relative;
}
.description-container {
flex: 1;
padding-left: 0.5em;
}
.attachment-buttons {
order: 99;
align-self: center;
}
}
a {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
svg {
color: inherit;
}
}
&.-loading {
cursor: progress;
}
&.-contain-fit {
img,
canvas {
object-fit: contain;
}
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
}
}
}

View file

@ -1,8 +1,7 @@
<template> <template>
<button <div
v-if="usePlaceholder" v-if="usePlaceholder"
class="Attachment -placeholder button-unstyled" :class="{ 'fullwidth': fullwidth }"
:class="classNames"
@click="openModal" @click="openModal"
> >
<a <a
@ -12,257 +11,312 @@
:href="attachment.url" :href="attachment.url"
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" :title="attachment.description"
@click.prevent
> >
<FAIcon :icon="placeholderIconClass" /> <FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }} <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a> </a>
<div </div>
v-if="edit || remove"
class="attachment-buttons"
>
<button
v-if="remove"
class="button-unstyled attachment-button"
@click.prevent="onRemove"
>
<FAIcon icon="trash-alt" />
</button>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
class="description-container"
:class="{ '-static': !edit }"
>
<input
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
<p v-else>
{{ localDescription }}
</p>
</div>
</button>
<div <div
v-else v-else
class="Attachment" v-show="!isEmpty"
:class="classNames" class="attachment"
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
> >
<div <a
v-show="!isEmpty" v-if="hidden"
class="attachment-wrapper" class="image-attachment"
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@click.prevent.stop="toggleHidden"
> >
<a <img
v-if="hidden" :key="nsfwImage"
class="image-container" class="nsfw"
:href="attachment.url" :src="nsfwImage"
:class="{'small': isSmall}"
>
<FAIcon
v-if="type === 'video'"
class="play-icon"
icon="play-circle"
/>
</a>
<button
v-if="nsfw && hideNsfwLocal && !hidden"
class="button-unstyled hider"
@click.prevent="toggleHidden"
>
<FAIcon icon="times" />
</button>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
class="image-attachment"
:class="{'hidden': hidden && preloadImage }"
:href="attachment.url"
target="_blank"
@click="openModal"
>
<StillImage
class="image"
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" />
@click.prevent.stop="toggleHidden" </a>
>
<img
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"
>
<FAIcon
v-if="type === 'video'"
class="play-icon"
icon="play-circle"
/>
</a>
<div
v-if="!hidden"
class="attachment-buttons"
>
<button
v-if="type === 'flash' && flashLoaded"
class="button-unstyled attachment-button"
:title="$t('status.attachment_stop_flash')"
@click.prevent="stopFlash"
>
<FAIcon icon="stop" />
</button>
<button
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
class="button-unstyled attachment-button"
:title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription"
>
<FAIcon icon="align-right" />
</button>
<button
v-if="!useModal && type !== 'unknown'"
class="button-unstyled attachment-button"
:title="$t('status.show_attachment_in_modal')"
@click.prevent="openModalForce"
>
<FAIcon icon="search-plus" />
</button>
<button
v-if="nsfw && hideNsfwLocal"
class="button-unstyled attachment-button"
:title="$t('status.hide_attachment')"
@click.prevent="toggleHidden"
>
<FAIcon icon="times" />
</button>
<button
v-if="shiftUp"
class="button-unstyled attachment-button"
:title="$t('status.move_up')"
@click.prevent="onShiftUp"
>
<FAIcon icon="chevron-left" />
</button>
<button
v-if="shiftDn"
class="button-unstyled attachment-button"
:title="$t('status.move_down')"
@click.prevent="onShiftDn"
>
<FAIcon icon="chevron-right" />
</button>
<button
v-if="remove"
class="button-unstyled attachment-button"
:title="$t('status.remove_attachment')"
@click.prevent="onRemove"
>
<FAIcon icon="trash-alt" />
</button>
</div>
<a <a
v-if="type === 'image' && (!hidden || preloadImage)" v-if="type === 'video' && !hidden"
class="image-container" class="video-container"
:class="{'-hidden': hidden && preloadImage }" :class="{'small': isSmall}"
:href="attachment.url" :href="allowPlay ? undefined : attachment.url"
target="_blank" @click="openModal"
@click.stop.prevent="openModal"
>
<StillImage
class="image"
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
:alt="attachment.description"
/>
</a>
<a
v-if="type === 'unknown' && !hidden"
class="placeholder-container"
:href="attachment.url"
target="_blank"
>
<FAIcon
size="5x"
:icon="placeholderIconClass"
/>
<p>
{{ localDescription }}
</p>
</a>
<component
:is="videoTag"
v-if="type === 'video' && !hidden"
class="video-container"
:class="{ 'button-unstyled': 'isModal' }"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<VideoAttachment
class="video"
:attachment="attachment"
:controls="!useModal"
@play="$emit('play')"
@pause="$emit('pause')"
/>
<FAIcon
v-if="useModal"
class="play-icon"
icon="play-circle"
/>
</component>
<span
v-if="type === 'audio' && !hidden"
class="audio-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<audio
v-if="type === 'audio'"
:src="attachment.url"
:alt="attachment.description"
:title="attachment.description"
controls
@play="$emit('play')"
@pause="$emit('pause')"
/>
</span>
<div
v-if="type === 'html' && attachment.oembed"
class="oembed-container"
@click.prevent="linkClicked"
>
<div
v-if="attachment.thumb_url"
class="image"
>
<img :src="attachment.thumb_url">
</div>
<div class="text">
<!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
<span
v-if="type === 'flash' && !hidden"
class="flash-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<Flash
ref="flash"
class="flash"
:src="attachment.large_thumb_url || attachment.url"
@playerOpened="setFlashLoaded(true)"
@playerClosed="setFlashLoaded(false)"
/>
</span>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
class="description-container"
:class="{ '-static': !edit }"
> >
<input <VideoAttachment
v-if="edit" class="video"
v-model="localDescription" :attachment="attachment"
type="text" :controls="allowPlay"
class="description-field" @play="$emit('play')"
:placeholder="$t('post_status.media_description')" @pause="$emit('pause')"
@keydown.enter.prevent="" />
<FAIcon
v-if="!allowPlay"
class="play-icon"
icon="play-circle"
/>
</a>
<audio
v-if="type === 'audio'"
:src="attachment.url"
:alt="attachment.description"
:title="attachment.description"
controls
@play="$emit('play')"
@pause="$emit('pause')"
/>
<div
v-if="type === 'html' && attachment.oembed"
class="oembed"
@click.prevent="linkClicked"
>
<div
v-if="attachment.thumb_url"
class="image"
> >
<p v-else> <img :src="attachment.thumb_url">
{{ localDescription }} </div>
</p> <div class="text">
<!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div> </div>
</div> </div>
</template> </template>
<script src="./attachment.js"></script> <script src="./attachment.js"></script>
<style src="./attachment.scss" lang="scss"></style> <style lang="scss">
@import '../../_variables.scss';
.attachments {
display: flex;
flex-wrap: wrap;
.non-gallery {
max-width: 100%;
}
.placeholder {
display: inline-block;
padding: 0.3em 1em 0.3em 0;
color: $fallback--link;
color: var(--postLink, $fallback--link);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
svg {
color: inherit;
}
}
.nsfw-placeholder {
cursor: pointer;
&.loading {
cursor: progress;
}
}
.attachment {
position: relative;
margin-top: 0.5em;
align-self: flex-start;
line-height: 0;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
.non-gallery.attachment {
&.video {
flex: 1 0 40%;
}
.nsfw {
height: 260px;
}
.small {
height: 120px;
flex-grow: 0;
}
.video {
height: 260px;
display: flex;
}
video {
max-height: 100%;
object-fit: contain;
}
}
.fullwidth {
flex-basis: 100%;
}
// fixes small gap below video
&.video {
line-height: 0;
}
.video-container {
display: flex;
max-height: 100%;
}
.video {
width: 100%;
height: 100%;
}
.play-icon {
position: absolute;
font-size: 64px;
top: calc(50% - 32px);
left: calc(50% - 32px);
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
}
.play-icon::before {
margin: 0;
}
&.html {
flex-basis: 90%;
width: 100%;
display: flex;
}
.hider {
position: absolute;
right: 0;
margin: 10px;
padding: 0;
z-index: 4;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
text-align: center;
width: 2em;
height: 2em;
font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color
background: rgba(230, 230, 230, 0.7);
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
}
video {
z-index: 0;
}
audio {
width: 100%;
}
img.media-upload {
line-height: 0;
max-height: 200px;
max-width: 100%;
}
.oembed {
line-height: 1.2em;
flex: 1 0 100%;
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: 5px;
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
.image-attachment {
&,
& .image {
width: 100%;
height: 100%;
}
&.hidden {
display: none;
}
.nsfw {
object-fit: cover;
width: 100%;
height: 100%;
}
img {
image-orientation: from-image; // NOTE: only FF supports this
}
}
}
</style>

View file

@ -1,4 +1,3 @@
import { h, resolveComponent } from 'vue'
import LoginForm from '../login_form/login_form.vue' import LoginForm from '../login_form/login_form.vue'
import MFARecoveryForm from '../mfa_form/recovery_form.vue' import MFARecoveryForm from '../mfa_form/recovery_form.vue'
import MFATOTPForm from '../mfa_form/totp_form.vue' import MFATOTPForm from '../mfa_form/totp_form.vue'
@ -6,8 +5,8 @@ import { mapGetters } from 'vuex'
const AuthForm = { const AuthForm = {
name: 'AuthForm', name: 'AuthForm',
render () { render (createElement) {
return h(resolveComponent(this.authForm)) return createElement('component', { is: this.authForm })
}, },
computed: { computed: {
authForm () { authForm () {

View file

@ -1,6 +1,5 @@
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = { const BasicUserCard = {
@ -14,8 +13,7 @@ const BasicUserCard = {
}, },
components: { components: {
UserCard, UserCard,
UserAvatar, UserAvatar
RichContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -4,7 +4,7 @@
<UserAvatar <UserAvatar
class="avatar" class="avatar"
:user="user" :user="user"
@click.prevent="toggleUserExpanded" @click.prevent.native="toggleUserExpanded"
/> />
</router-link> </router-link>
<div <div
@ -25,11 +25,17 @@
:title="user.name" :title="user.name"
class="basic-user-card-user-name" class="basic-user-card-user-name"
> >
<RichContent <!-- eslint-disable vue/no-v-html -->
<span
v-if="user.name_html"
class="basic-user-card-user-name-value" class="basic-user-card-user-name-value"
:html="user.name" v-html="user.name_html"
:emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html -->
<span
v-else
class="basic-user-card-user-name-value"
>{{ user.name }}</span>
</div> </div>
<div> <div>
<router-link <router-link

View file

@ -9,7 +9,7 @@ const Bookmarks = {
components: { components: {
Timeline Timeline
}, },
unmounted () { destroyed () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
} }
} }

View file

@ -57,7 +57,7 @@ const Chat = {
}) })
this.setChatLayout() this.setChatLayout()
}, },
unmounted () { destroyed () {
window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleLayoutChange) window.removeEventListener('resize', this.handleLayoutChange)
this.unsetChatLayout() this.unsetChatLayout()

View file

@ -26,71 +26,73 @@
/> />
</div> </div>
</div> </div>
<div <template>
ref="scrollable"
class="scrollable-message-list"
:style="{ height: scrollableContainerHeight }"
@scroll="handleScroll"
>
<template v-if="!errorLoadingChat">
<ChatMessage
v-for="chatViewItem in chatViewItems"
:key="chatViewItem.id"
:author="recipient"
:chat-view-item="chatViewItem"
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
@hover="onMessageHover"
/>
</template>
<div <div
v-else ref="scrollable"
class="chat-loading-error" class="scrollable-message-list"
:style="{ height: scrollableContainerHeight }"
@scroll="handleScroll"
> >
<div class="alert error"> <template v-if="!errorLoadingChat">
{{ $t('chats.error_loading_chat') }} <ChatMessage
v-for="chatViewItem in chatViewItems"
:key="chatViewItem.id"
:author="recipient"
:chat-view-item="chatViewItem"
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
@hover="onMessageHover"
/>
</template>
<div
v-else
class="chat-loading-error"
>
<div class="alert error">
{{ $t('chats.error_loading_chat') }}
</div>
</div> </div>
</div> </div>
</div>
<div
ref="footer"
class="panel-body footer"
>
<div <div
class="jump-to-bottom-button" ref="footer"
:class="{ 'visible': jumpToBottomButtonVisible }" class="panel-body footer"
@click="scrollDown({ behavior: 'smooth' })"
> >
<span> <div
<FAIcon icon="chevron-down" /> class="jump-to-bottom-button"
<div :class="{ 'visible': jumpToBottomButtonVisible }"
v-if="newMessageCount" @click="scrollDown({ behavior: 'smooth' })"
class="badge badge-notification unread-chat-count unread-message-count" >
> <span>
{{ newMessageCount }} <FAIcon icon="chevron-down" />
</div> <div
</span> v-if="newMessageCount"
class="badge badge-notification unread-chat-count unread-message-count"
>
{{ newMessageCount }}
</div>
</span>
</div>
<PostStatusForm
:disable-subject="true"
:disable-scope-selector="true"
:disable-notice="true"
:disable-lock-warning="true"
:disable-polls="true"
:disable-sensitivity-checkbox="true"
:disable-submit="errorLoadingChat || !currentChat"
:disable-preview="true"
:optimistic-posting="true"
:post-handler="sendMessage"
:submit-on-enter="!mobileLayout"
:preserve-focus="!mobileLayout"
:auto-focus="!mobileLayout"
:placeholder="formPlaceholder"
:file-limit="1"
max-height="160"
emoji-picker-placement="top"
@resize="handleResize"
/>
</div> </div>
<PostStatusForm </template>
:disable-subject="true"
:disable-scope-selector="true"
:disable-notice="true"
:disable-lock-warning="true"
:disable-polls="true"
:disable-sensitivity-checkbox="true"
:disable-submit="errorLoadingChat || !currentChat"
:disable-preview="true"
:optimistic-posting="true"
:post-handler="sendMessage"
:submit-on-enter="!mobileLayout"
:preserve-focus="!mobileLayout"
:auto-focus="!mobileLayout"
:placeholder="formPlaceholder"
:file-limit="1"
max-height="160"
emoji-picker-placement="top"
@resize="handleResize"
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -23,7 +23,10 @@
class="timeline" class="timeline"
> >
<List :items="sortedChatList"> <List :items="sortedChatList">
<template v-slot:item="{item}"> <template
slot="item"
slot-scope="{item}"
>
<ChatListItem <ChatListItem
:key="item.id" :key="item.id"
:compact="false" :compact="false"

View file

@ -1,5 +1,5 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import StatusBody from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
@ -16,7 +16,7 @@ const ChatListItem = {
AvatarList, AvatarList,
Timeago, Timeago,
ChatTitle, ChatTitle,
StatusBody StatusContent
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -38,14 +38,12 @@ const ChatListItem = {
}, },
messageForStatusContent () { messageForStatusContent () {
const message = this.chat.lastMessage const message = this.chat.lastMessage
const messageEmojis = message ? message.emojis : []
const isYou = message && message.account_id === this.currentUser.id const isYou = message && message.account_id === this.currentUser.id
const content = message ? (this.attachmentInfo || message.content) : '' const content = message ? (this.attachmentInfo || message.content) : ''
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
return { return {
summary: '', summary: '',
emojis: messageEmojis, statusnet_html: messagePreview,
raw_html: messagePreview,
text: messagePreview, text: messagePreview,
attachments: [] attachments: []
} }

View file

@ -77,15 +77,18 @@
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
} }
.chat-preview-body { .StatusContent {
--emoji-size: 1.4em; img.emoji {
width: 1.4em;
height: 1.4em;
}
} }
.time-wrapper { .time-wrapper {
line-height: 1.4em; line-height: 1.4em;
} }
.chat-preview-body { .single-line {
padding-right: 1em; padding-right: 1em;
} }
} }

View file

@ -29,8 +29,7 @@
</div> </div>
</div> </div>
<div class="chat-preview"> <div class="chat-preview">
<StatusBody <StatusContent
class="chat-preview-body"
:status="messageForStatusContent" :status="messageForStatusContent"
:single-line="true" :single-line="true"
/> />

View file

@ -27,7 +27,6 @@ const ChatMessage = {
'chatViewItem', 'chatViewItem',
'hoveredMessageChain' 'hoveredMessageChain'
], ],
emits: ['hover'],
components: { components: {
Popover, Popover,
Attachment, Attachment,
@ -58,9 +57,8 @@ const ChatMessage = {
messageForStatusContent () { messageForStatusContent () {
return { return {
summary: '', summary: '',
emojis: this.message.emojis, statusnet_html: this.message.content,
raw_html: this.message.content || '', text: this.message.content,
text: this.message.content || '',
attachments: this.message.attachments attachments: this.message.attachments
} }
}, },

View file

@ -1,7 +1,6 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.chat-message-wrapper { .chat-message-wrapper {
&.hovered-message-chain { &.hovered-message-chain {
.animated.Avatar { .animated.Avatar {
canvas { canvas {
@ -41,12 +40,6 @@
.chat-message { .chat-message {
display: flex; display: flex;
padding-bottom: 0.5em; padding-bottom: 0.5em;
.status-body:hover {
--_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
}
} }
.avatar-wrapper { .avatar-wrapper {
@ -69,6 +62,10 @@
&.with-media { &.with-media {
width: 100%; width: 100%;
.gallery-row {
overflow: hidden;
}
.status { .status {
width: 100%; width: 100%;
} }
@ -92,9 +89,8 @@
} }
.without-attachment { .without-attachment {
.message-content { .status-content {
// TODO figure out how to do it properly &::after {
.RichContent::after {
margin-right: 5.4em; margin-right: 5.4em;
content: " "; content: " ";
display: inline-block; display: inline-block;
@ -166,7 +162,6 @@
.visible { .visible {
opacity: 1; opacity: 1;
} }
} }
.chat-message-date-separator { .chat-message-date-separator {

View file

@ -50,7 +50,7 @@
@show="menuOpened = true" @show="menuOpened = true"
@close="menuOpened = false" @close="menuOpened = false"
> >
<template v-slot:content> <div slot="content">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -59,29 +59,26 @@
<FAIcon icon="times" /> {{ $t("chats.delete") }} <FAIcon icon="times" /> {{ $t("chats.delete") }}
</button> </button>
</div> </div>
</template> </div>
<template v-slot:trigger> <button
<button slot="trigger"
class="button-default menu-icon" class="button-default menu-icon"
:title="$t('chats.more')" :title="$t('chats.more')"
> >
<FAIcon icon="ellipsis-h" /> <FAIcon icon="ellipsis-h" />
</button> </button>
</template>
</Popover> </Popover>
</div> </div>
<StatusContent <StatusContent
class="message-content"
:status="messageForStatusContent" :status="messageForStatusContent"
:full-content="true" :full-content="true"
> >
<template v-slot:footer> <span
<span slot="footer"
class="created-at" class="created-at"
> >
{{ createdAt }} {{ createdAt }}
</span> </span>
</template>
</StatusContent> </StatusContent>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ library.add(
faTimes faTimes
) )
const shoutPanel = { const chatPanel = {
props: [ 'floating' ], props: [ 'floating' ],
data () { data () {
return { return {
@ -21,12 +21,12 @@ const shoutPanel = {
}, },
computed: { computed: {
messages () { messages () {
return this.$store.state.shout.messages return this.$store.state.chat.messages
} }
}, },
methods: { methods: {
submit (message) { submit (message) {
this.$store.state.shout.channel.push('new_msg', { text: message }, 10000) this.$store.state.chat.channel.push('new_msg', { text: message }, 10000)
this.currentMessage = '' this.currentMessage = ''
}, },
togglePanel () { togglePanel () {
@ -50,4 +50,4 @@ const shoutPanel = {
} }
} }
export default shoutPanel export default chatPanel

View file

@ -1,12 +1,12 @@
<template> <template>
<div <div
v-if="!collapsed || !floating" v-if="!collapsed || !floating"
class="shout-panel" class="chat-panel"
> >
<div class="panel panel-default"> <div class="panel panel-default">
<div <div
class="panel-heading timeline-heading" class="panel-heading timeline-heading"
:class="{ 'shout-heading': floating }" :class="{ 'chat-heading': floating }"
@click.stop.prevent="togglePanel" @click.stop.prevent="togglePanel"
> >
<div class="title"> <div class="title">
@ -18,33 +18,33 @@
/> />
</div> </div>
</div> </div>
<div class="shout-window"> <div class="chat-window">
<div <div
v-for="message in messages" v-for="message in messages"
:key="message.id" :key="message.id"
class="shout-message" class="chat-message"
> >
<span class="shout-avatar"> <span class="chat-avatar">
<img :src="message.author.avatar"> <img :src="message.author.avatar">
</span> </span>
<div class="shout-content"> <div class="chat-content">
<router-link <router-link
class="shout-name" class="chat-name"
:to="userProfileLink(message.author)" :to="userProfileLink(message.author)"
> >
{{ message.author.username }} {{ message.author.username }}
</router-link> </router-link>
<br> <br>
<span class="shout-text"> <span class="chat-text">
{{ message.text }} {{ message.text }}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="shout-input"> <div class="chat-input">
<textarea <textarea
v-model="currentMessage" v-model="currentMessage"
class="shout-input-textarea" class="chat-input-textarea"
rows="1" rows="1"
@keyup.enter="submit(currentMessage)" @keyup.enter="submit(currentMessage)"
/> />
@ -53,11 +53,11 @@
</div> </div>
<div <div
v-else v-else
class="shout-panel" class="chat-panel"
> >
<div class="panel panel-default"> <div class="panel panel-default">
<div <div
class="panel-heading stub timeline-heading shout-heading" class="panel-heading stub timeline-heading chat-heading"
@click.stop.prevent="togglePanel" @click.stop.prevent="togglePanel"
> >
<div class="title"> <div class="title">
@ -72,33 +72,26 @@
</div> </div>
</template> </template>
<script src="./shout_panel.js"></script> <script src="./chat_panel.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.floating-shout { .floating-chat {
position: fixed; position: fixed;
right: 0px;
bottom: 0px; bottom: 0px;
z-index: 1000; z-index: 1000;
max-width: 25em; max-width: 25em;
} }
.floating-shout.left { .chat-panel {
left: 0px; .chat-heading {
}
.floating-shout:not(.left) {
right: 0px;
}
.shout-panel {
.shout-heading {
cursor: pointer; cursor: pointer;
.icon { .icon {
color: $fallback--text; color: $fallback--text;
color: var(--panelText, $fallback--text); color: var(--text, $fallback--text);
margin-right: 0.5em; margin-right: 0.5em;
} }
@ -109,22 +102,22 @@
} }
} }
.shout-window { .chat-window {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
max-height: 20em; max-height: 20em;
} }
.shout-window-container { .chat-window-container {
height: 100%; height: 100%;
} }
.shout-message { .chat-message {
display: flex; display: flex;
padding: 0.2em 0.5em padding: 0.2em 0.5em
} }
.shout-avatar { .chat-avatar {
img { img {
height: 24px; height: 24px;
width: 24px; width: 24px;
@ -135,7 +128,7 @@
} }
} }
.shout-input { .chat-input {
display: flex; display: flex;
textarea { textarea {
flex: 1; flex: 1;
@ -145,7 +138,7 @@
} }
} }
.shout-panel { .chat-panel {
.title { .title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -1,12 +1,11 @@
import Vue from 'vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
export default { export default Vue.component('chat-title', {
name: 'ChatTitle', name: 'ChatTitle',
components: { components: {
UserAvatar, UserAvatar
RichContent
}, },
props: [ props: [
'user', 'withAvatar' 'user', 'withAvatar'
@ -24,4 +23,4 @@ export default {
return generateProfileLink(user.id, user.screen_name) return generateProfileLink(user.id, user.screen_name)
} }
} }
} })

View file

@ -1,4 +1,5 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div <div
class="chat-title" class="chat-title"
:title="title" :title="title"
@ -13,14 +14,12 @@
height="23px" height="23px"
/> />
</router-link> </router-link>
<RichContent <span
v-if="user"
class="username" class="username"
:title="'@'+user.screen_name_ui" v-html="htmlTitle"
:html="htmlTitle"
:emoji="user.emoji || []"
/> />
</div> </div>
<!-- eslint-enable vue/no-v-html -->
</template> </template>
<script src="./chat_title.js"></script> <script src="./chat_title.js"></script>
@ -35,8 +34,6 @@
white-space: nowrap; white-space: nowrap;
align-items: center; align-items: center;
--emoji-size: 14px;
.username { .username {
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -44,6 +41,14 @@
display: inline; display: inline;
word-wrap: break-word; word-wrap: break-word;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
} }
.Avatar { .Avatar {

View file

@ -6,9 +6,9 @@
<input <input
type="checkbox" type="checkbox"
:disabled="disabled" :disabled="disabled"
:checked="modelValue" :checked="checked"
:indeterminate="indeterminate" :indeterminate.prop="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('change', $event.target.checked)"
> >
<i class="checkbox-indicator" /> <i class="checkbox-indicator" />
<span <span
@ -22,9 +22,12 @@
<script> <script>
export default { export default {
emits: ['update:modelValue'], model: {
prop: 'checked',
event: 'change'
},
props: [ props: [
'modelValue', 'checked',
'indeterminate', 'indeterminate',
'disabled' 'disabled'
] ]

View file

@ -11,28 +11,28 @@
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined' && showOptionalTickbox" v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
:model-value="present" :checked="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
:id="name + '-t'" :id="name + '-t'"
class="textColor unstyled" class="textColor unstyled"
type="text" type="text"
:value="modelValue || fallback" :value="value || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('input', $event.target.value)"
> >
<input <input
v-if="validColor" v-if="validColor"
:id="name" :id="name"
class="nativeColor unstyled" class="nativeColor unstyled"
type="color" type="color"
:value="modelValue || fallback" :value="value || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('input', $event.target.value)"
> >
<div <div
v-if="transparentColor" v-if="transparentColor"
@ -67,7 +67,7 @@ export default {
}, },
// Color value, should be required but vue cannot tell the difference // Color value, should be required but vue cannot tell the difference
// between "property missing" and "property set to undefined" // between "property missing" and "property set to undefined"
modelValue: { value: {
required: false, required: false,
type: String, type: String,
default: undefined default: undefined
@ -91,19 +91,18 @@ export default {
default: true default: true
} }
}, },
emits: ['update:modelValue'],
computed: { computed: {
present () { present () {
return typeof this.modelValue !== 'undefined' return typeof this.value !== 'undefined'
}, },
validColor () { validColor () {
return hex2rgb(this.modelValue || this.fallback) return hex2rgb(this.value || this.fallback)
}, },
transparentColor () { transparentColor () {
return this.modelValue === 'transparent' return this.value === 'transparent'
}, },
computedColor () { computedColor () {
return this.modelValue && this.modelValue.startsWith('--') return this.value && this.value.startsWith('--')
} }
} }
} }

View file

@ -1,19 +1,5 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -49,10 +35,7 @@ const conversation = {
data () { data () {
return { return {
highlight: null, highlight: null,
expanded: false, expanded: false
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {},
inlineDivePosition: null
} }
}, },
props: [ props: [
@ -70,50 +53,12 @@ const conversation = {
} }
}, },
computed: { computed: {
maxDepthToShowByDefault () {
// maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children"
// there is a -2 here
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay
},
isTreeView () {
return !this.isLinearView
},
treeViewIsSimple () {
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
},
isLinearView () {
return this.displayStyle === 'linear'
},
shouldFadeAncestors () {
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
},
otherRepliesButtonPosition () {
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
},
showOtherRepliesButtonBelowStatus () {
return this.otherRepliesButtonPosition === 'below'
},
showOtherRepliesButtonInsideStatus () {
return this.otherRepliesButtonPosition === 'inside'
},
suspendable () {
if (this.isTreeView) {
return Object.entries(this.statusContentProperties)
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
}
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every(s => s.suspendable)
} else {
return true
}
},
hideStatus () { hideStatus () {
return this.virtualHidden && this.suspendable if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
} else {
return this.virtualHidden
}
}, },
status () { status () {
return this.$store.state.statuses.allStatusesObject[this.statusId] return this.$store.state.statuses.allStatusesObject[this.statusId]
@ -145,121 +90,6 @@ const conversation = {
return sortAndFilterConversation(conversation, this.status) return sortAndFilterConversation(conversation, this.status)
}, },
statusMap () {
return this.conversation.reduce((res, s) => {
res[s.id] = s
return res
}, {})
},
threadTree () {
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
const threads = this.conversation.reduce((a, cur) => {
const id = cur.id
a.forest[id] = this.getReplies(id)
.map(s => s.id)
return a
}, {
forest: {}
})
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) {
return []
}
processed[id] = true
return [{
status: this.conversation[reverseLookupTable[id]],
id,
depth
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
}).reduce((a, b) => a.concat(b), [])
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
return linearized
},
replyIds () {
return this.conversation.map(k => k.id)
.reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id)
return res
}, {})
},
totalReplyCount () {
const sizes = {}
const subTreeSizeFor = (id) => {
if (sizes[id]) {
return sizes[id]
}
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
return sizes[id]
}
this.conversation.map(k => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself
return res
}, {})
},
totalReplyDepth () {
const depths = {}
const subTreeDepthFor = (id) => {
if (depths[id]) {
return depths[id]
}
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
return depths[id]
}
this.conversation.map(k => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself
return res
}, {})
},
depths () {
return this.threadTree.reduce((a, k) => {
a[k.id] = k.depth
return a
}, {})
},
topLevel () {
const topLevel = this.conversation.reduce((tl, cur) =>
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
return topLevel
},
otherTopLevelCount () {
return this.topLevel.length - 1
},
showingTopLevel () {
if (this.canDive && this.diveRoot) {
return [this.statusMap[this.diveRoot]]
}
return this.topLevel
},
diveRoot () {
const statusId = this.inlineDivePosition || this.statusId
const isTopLevel = !this.parentOf(statusId)
return isTopLevel ? null : statusId
},
diveDepth () {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
},
diveMode () {
return this.canDive && !!this.diveRoot
},
shouldShowAllConversationButton () {
// The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
},
shouldShowAncestors () {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
},
replies () { replies () {
let i = 1 let i = 1
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -279,71 +109,15 @@ const conversation = {
}, {}) }, {})
}, },
isExpanded () { isExpanded () {
return !!(this.expanded || this.isPage) return this.expanded || this.isPage
}, },
hiddenStyle () { hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px' const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {} return this.virtualHidden ? { height } : {}
},
threadDisplayStatus () {
return this.conversation.reduce((a, k) => {
const id = k.id
const depth = this.depths[id]
const status = (() => {
if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id]
}
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
return 'showing'
} else {
return 'hidden'
}
})()
a[id] = status
return a
}, {})
},
statusContentProperties () {
return this.conversation.reduce((a, k) => {
const id = k.id
const props = (() => {
const def = {
showingTall: false,
expandingSubject: false,
showingLongSubject: false,
isReplying: false,
mediaPlaying: []
}
if (this.statusContentPropertiesObject[id]) {
return {
...def,
...this.statusContentPropertiesObject[id]
}
}
return def
})()
a[id] = props
return a
}, {})
},
canDive () {
return this.isTreeView && this.isExpanded
},
focused () {
return (id) => {
return (this.isExpanded) && id === this.highlight
}
},
maybeHighlight () {
return this.isExpanded ? this.highlight : null
} }
}, },
components: { components: {
Status, Status
ThreadTree
}, },
watch: { watch: {
statusId (newVal, oldVal) { statusId (newVal, oldVal) {
@ -358,8 +132,6 @@ const conversation = {
expanded (value) { expanded (value) {
if (value) { if (value) {
this.fetchConversation() this.fetchConversation()
} else {
this.resetDisplayState()
} }
}, },
virtualHidden (value) { virtualHidden (value) {
@ -389,8 +161,8 @@ const conversation = {
getReplies (id) { getReplies (id) {
return this.replies[id] || [] return this.replies[id] || []
}, },
getHighlight () { focused (id) {
return this.isExpanded ? this.highlight : null return (this.isExpanded) && id === this.statusId
}, },
setHighlight (id) { setHighlight (id) {
if (!id) return if (!id) return
@ -398,139 +170,15 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id) this.$store.dispatch('fetchEmojiReactionsBy', id)
}, },
getHighlight () {
return this.isExpanded ? this.highlight : null
},
toggleExpanded () { toggleExpanded () {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
getConversationId (statusId) { getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
},
setThreadDisplay (id, nextStatus) {
this.threadDisplayStatusObject = {
...this.threadDisplayStatusObject,
[id]: nextStatus
}
},
toggleThreadDisplay (id) {
const curStatus = this.threadDisplayStatus[id]
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
this.setThreadDisplay(id, nextStatus)
},
setThreadDisplayRecursively (id, nextStatus) {
this.setThreadDisplay(id, nextStatus)
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
},
showThreadRecursively (id) {
this.setThreadDisplayRecursively(id, 'showing')
},
setStatusContentProperty (id, name, value) {
this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject,
[id]: {
...this.statusContentPropertiesObject[id],
[name]: value
}
}
},
toggleStatusContentProperty (id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
},
leastVisibleAncestor (id) {
let cur = id
let parent = this.parentOf(cur)
while (cur) {
// if the parent is showing it means cur is visible
if (this.threadDisplayStatus[parent] === 'showing') {
return cur
}
parent = this.parentOf(parent)
cur = this.parentOf(cur)
}
// nothing found, fall back to toplevel
return this.topLevel[0] ? this.topLevel[0].id : undefined
},
diveIntoStatus (id, preventScroll) {
this.tryScrollTo(id)
},
diveToTopLevel () {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
},
// only used when we are not on a page
undive () {
this.inlineDivePosition = null
this.setHighlight(this.statusId)
},
tryScrollTo (id) {
if (!id) {
return
}
if (this.isPage) {
// set statusId
this.$router.push({ name: 'conversation', params: { id } })
} else {
this.inlineDivePosition = id
}
// Because the conversation can be unmounted when out of sight
// and mounted again when it comes into sight,
// the `mounted` or `created` function in `status` should not
// contain scrolling calls, as we do not want the page to jump
// when we scroll with an expanded conversation.
//
// Now the method is to rely solely on the `highlight` watcher
// in `status` components.
// In linear views, all statuses are rendered at all times, but
// in tree views, it is possible that a change in active status
// removes and adds status components (e.g. an originally child
// status becomes an ancestor status, and thus they will be
// different).
// Here, let the components be rendered first, in order to trigger
// the `highlight` watcher.
this.$nextTick(() => {
this.setHighlight(id)
})
},
goToCurrent () {
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
},
statusById (id) {
return this.statusMap[id]
},
parentOf (id) {
const status = this.statusById(id)
if (!status) {
return undefined
}
const { in_reply_to_status_id: parentId } = status
if (!this.statusMap[parentId]) {
return undefined
}
return parentId
},
parentOrSelf (id) {
return this.parentOf(id) || id
},
// Ancestors of some status, from top to bottom
ancestorsOf (id) {
const ancestors = []
let cur = this.parentOf(id)
while (cur) {
ancestors.unshift(this.statusMap[cur])
cur = this.parentOf(cur)
}
return ancestors
},
topLevelAncestorOrSelfId (id) {
let cur = id
let parent = this.parentOf(id)
while (parent) {
cur = this.parentOf(cur)
parent = this.parentOf(parent)
}
return cur
},
resetDisplayState () {
this.undive()
this.threadDisplayStatusObject = {}
} }
} }
} }

View file

@ -18,176 +18,24 @@
{{ $t('timeline.collapse') }} {{ $t('timeline.collapse') }}
</button> </button>
</div> </div>
<div class="conversation-body panel-body"> <status
<div v-for="status in conversation"
v-if="isTreeView" :key="status.id"
class="thread-body" ref="statusComponent"
> :inline-expanded="collapsable && isExpanded"
<div :statusoid="status"
v-if="shouldShowAllConversationButton" :expandable="!isExpanded"
class="conversation-dive-to-top-level-box" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
> :focused="focused(status.id)"
<i18n-t :in-conversation="isExpanded"
keypath="status.show_all_conversation_with_icon" :highlight="getHighlight()"
tag="button" :replies="getReplies(status.id)"
class="button-unstyled -link" :in-profile="inProfile"
@click.prevent="diveToTopLevel" :profile-user-id="profileUserId"
scope="global" class="conversation-status status-fadein panel-body"
> @goto="setHighlight"
<template #icon> @toggleExpanded="toggleExpanded"
<FAIcon />
icon="angle-double-left"
/>
</template>
<template #text>
<span>
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
</span>
</template>
</i18n-t>
</div>
<div
v-if="shouldShowAncestors"
class="thread-ancestors"
>
<div
v-for="status in ancestorsOf(diveRoot)"
:key="status.id"
class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
>
<status
ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="focused(status.id)"
:in-conversation="isExpanded"
:highlight="getHighlight()"
:replies="getReplies(status.id)"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body"
:simple-tree="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:dive="() => diveIntoStatus(status.id)"
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
:controlled-replying="statusContentProperties[status.id].replying"
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
class="thread-ancestor-dive-box"
>
<div
class="thread-ancestor-dive-box-inner"
>
<i18n-t
tag="button"
scope="global"
keypath="status.ancestor_follow_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="diveIntoStatus(status.id)"
>
<template #icon>
<FAIcon
icon="angle-double-right"
/>
</template>
<template #text>
<span>
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
</span>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
<thread-tree
v-for="status in showingTopLevel"
:key="status.id"
ref="statusComponent"
:depth="0"
:status="status"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:focused="focused"
:get-replies="getReplies"
:highlight="maybeHighlight"
:set-highlight="setHighlight"
:toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth"
:status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty"
:dive="canDive ? diveIntoStatus : undefined"
/>
</div>
<div
v-if="isLinearView"
class="thread-body"
>
<status
v-for="status in conversation"
:key="status.id"
ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="focused(status.id)"
:in-conversation="isExpanded"
:highlight="getHighlight()"
:replies="getReplies(status.id)"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth"
:status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
</div>
</div>
</div> </div>
<div <div
v-else v-else
@ -201,45 +49,6 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.Conversation { .Conversation {
.conversation-dive-to-top-level-box {
padding: var(--status-margin, $status-margin);
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0;
/* Make the button stretch along the whole row */
display: flex;
align-items: stretch;
flex-direction: column;
}
.thread-ancestors {
margin-left: var(--status-margin, $status-margin);
border-left: 2px solid var(--border, $fallback--border);
}
.thread-ancestor.-faded .StatusContent {
--link: var(--faintLink);
--text: var(--faint);
color: var(--text);
}
.thread-ancestor-dive-box {
padding-left: var(--status-margin, $status-margin);
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0;
/* Make the button stretch along the whole row */
&, &-inner {
display: flex;
align-items: stretch;
flex-direction: column;
}
}
.thread-ancestor-dive-box-inner {
padding: var(--status-margin, $status-margin);
}
.conversation-status { .conversation-status {
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
@ -247,28 +56,12 @@
border-radius: 0; border-radius: 0;
} }
.thread-ancestor-has-other-replies .conversation-status, &.-expanded {
.thread-ancestor:last-child .conversation-status, .conversation-status:last-child {
.thread-ancestor:last-child .thread-ancestor-dive-box, border-bottom: none;
&.-expanded .thread-tree .conversation-status { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-bottom: none; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
} }
.thread-ancestors + .thread-tree > .conversation-status {
border-top-width: 1px;
border-top-style: solid;
border-top-color: var(--border, $fallback--border);
}
/* expanded conversation in timeline */
&.status-fadein.-expanded .thread-body {
border-left-width: 4px;
border-left-style: solid;
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border);
} }
} }
</style> </style>

View file

@ -34,7 +34,7 @@
<search-bar <search-bar
v-if="currentUser || !privateMode" v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop @click.stop.native
/> />
<button <button
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@ -52,7 +52,6 @@
href="/pleroma/admin/#/login-pleroma" href="/pleroma/admin/#/login-pleroma"
class="nav-icon" class="nav-icon"
target="_blank" target="_blank"
@click.stop
> >
<FAIcon <FAIcon
fixed-width fixed-width

View file

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

View file

@ -31,7 +31,6 @@ library.add(
*/ */
const EmojiInput = { const EmojiInput = {
emits: ['update:modelValue', 'shown'],
props: { props: {
suggest: { suggest: {
/** /**
@ -58,7 +57,7 @@ const EmojiInput = {
required: true, required: true,
type: Function type: Function
}, },
modelValue: { value: {
/** /**
* Used for v-model * Used for v-model
*/ */
@ -137,38 +136,39 @@ const EmojiInput = {
return (this.wordAtCaret || {}).word || '' return (this.wordAtCaret || {}).word || ''
}, },
wordAtCaret () { wordAtCaret () {
if (this.modelValue && this.caret) { if (this.value && this.caret) {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
return word return word
} }
} }
}, },
mounted () { mounted () {
const { root } = this.$refs const slots = this.$slots.default
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') if (!slots || slots.length === 0) return
const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag))
if (!input) return if (!input) return
this.input = input this.input = input
this.resize() this.resize()
input.addEventListener('blur', this.onBlur) input.elm.addEventListener('blur', this.onBlur)
input.addEventListener('focus', this.onFocus) input.elm.addEventListener('focus', this.onFocus)
input.addEventListener('paste', this.onPaste) input.elm.addEventListener('paste', this.onPaste)
input.addEventListener('keyup', this.onKeyUp) input.elm.addEventListener('keyup', this.onKeyUp)
input.addEventListener('keydown', this.onKeyDown) input.elm.addEventListener('keydown', this.onKeyDown)
input.addEventListener('click', this.onClickInput) input.elm.addEventListener('click', this.onClickInput)
input.addEventListener('transitionend', this.onTransition) input.elm.addEventListener('transitionend', this.onTransition)
input.addEventListener('input', this.onInput) input.elm.addEventListener('input', this.onInput)
}, },
unmounted () { unmounted () {
const { input } = this const { input } = this
if (input) { if (input) {
input.removeEventListener('blur', this.onBlur) input.elm.removeEventListener('blur', this.onBlur)
input.removeEventListener('focus', this.onFocus) input.elm.removeEventListener('focus', this.onFocus)
input.removeEventListener('paste', this.onPaste) input.elm.removeEventListener('paste', this.onPaste)
input.removeEventListener('keyup', this.onKeyUp) input.elm.removeEventListener('keyup', this.onKeyUp)
input.removeEventListener('keydown', this.onKeyDown) input.elm.removeEventListener('keydown', this.onKeyDown)
input.removeEventListener('click', this.onClickInput) input.elm.removeEventListener('click', this.onClickInput)
input.removeEventListener('transitionend', this.onTransition) input.elm.removeEventListener('transitionend', this.onTransition)
input.removeEventListener('input', this.onInput) input.elm.removeEventListener('input', this.onInput)
} }
}, },
watch: { watch: {
@ -189,11 +189,8 @@ const EmojiInput = {
img: imageUrl || '' img: imageUrl || ''
})) }))
}, },
suggestions: { suggestions (newValue) {
handler (newValue) { this.$nextTick(this.resize)
this.$nextTick(this.resize)
},
deep: true
} }
}, },
methods: { methods: {
@ -219,7 +216,7 @@ const EmojiInput = {
}, 0) }, 0)
}, },
togglePicker () { togglePicker () {
this.input.focus() this.input.elm.focus()
this.showPicker = !this.showPicker this.showPicker = !this.showPicker
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
@ -228,13 +225,13 @@ const EmojiInput = {
} }
}, },
replace (replacement) { replace (replacement) {
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('update:modelValue', newValue) this.$emit('input', newValue)
this.caret = 0 this.caret = 0
}, },
insert ({ insertion, keepOpen, surroundingSpace = true }) { insert ({ insertion, keepOpen, surroundingSpace = true }) {
const before = this.modelValue.substring(0, this.caret) || '' const before = this.value.substring(0, this.caret) || ''
const after = this.modelValue.substring(this.caret) || '' const after = this.value.substring(this.caret) || ''
/* Using a bit more smart approach to padding emojis with spaces: /* Using a bit more smart approach to padding emojis with spaces:
* - put a space before cursor if there isn't one already, unless we * - put a space before cursor if there isn't one already, unless we
@ -262,16 +259,16 @@ const EmojiInput = {
after after
].join('') ].join('')
this.keepOpen = keepOpen this.keepOpen = keepOpen
this.$emit('update:modelValue', newValue) this.$emit('input', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length const position = this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) { if (!keepOpen) {
this.input.focus() this.input.elm.focus()
} }
this.$nextTick(function () { this.$nextTick(function () {
// Re-focus inputbox after clicking suggestion // Re-focus inputbox after clicking suggestion
// Set selection right after the replacement instead of the very end // Set selection right after the replacement instead of the very end
this.input.setSelectionRange(position, position) this.input.elm.setSelectionRange(position, position)
this.caret = position this.caret = position
}) })
}, },
@ -281,16 +278,16 @@ const EmojiInput = {
if (len > 0 || suggestion) { if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('update:modelValue', newValue) this.$emit('input', newValue)
this.highlighted = 0 this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length const position = this.wordAtCaret.start + replacement.length
this.$nextTick(function () { this.$nextTick(function () {
// Re-focus inputbox after clicking suggestion // Re-focus inputbox after clicking suggestion
this.input.focus() this.input.elm.focus()
// Set selection right after the replacement instead of the very end // Set selection right after the replacement instead of the very end
this.input.setSelectionRange(position, position) this.input.elm.setSelectionRange(position, position)
this.caret = position this.caret = position
}) })
e.preventDefault() e.preventDefault()
@ -352,7 +349,7 @@ const EmojiInput = {
} }
this.$nextTick(() => { this.$nextTick(() => {
const { offsetHeight } = this.input const { offsetHeight } = this.input.elm
const { picker } = this.$refs const { picker } = this.$refs
const pickerBottom = picker.$el.getBoundingClientRect().bottom const pickerBottom = picker.$el.getBoundingClientRect().bottom
if (pickerBottom > window.innerHeight) { if (pickerBottom > window.innerHeight) {
@ -417,8 +414,8 @@ const EmojiInput = {
// Scroll the input element to the position of the cursor // Scroll the input element to the position of the cursor
this.$nextTick(() => { this.$nextTick(() => {
this.input.blur() this.input.elm.blur()
this.input.focus() this.input.elm.focus()
}) })
} }
// Disable suggestions hotkeys if suggestions are hidden // Disable suggestions hotkeys if suggestions are hidden
@ -447,7 +444,7 @@ const EmojiInput = {
// de-focuses the element (i.e. default browser behavior) // de-focuses the element (i.e. default browser behavior)
if (key === 'Escape') { if (key === 'Escape') {
if (!this.temporarilyHideSuggestions) { if (!this.temporarilyHideSuggestions) {
this.input.focus() this.input.elm.focus()
} }
} }
@ -458,7 +455,7 @@ const EmojiInput = {
this.showPicker = false this.showPicker = false
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()
this.$emit('update:modelValue', e.target.value) this.$emit('input', e.target.value)
}, },
onClickInput (e) { onClickInput (e) {
this.showPicker = false this.showPicker = false
@ -483,7 +480,7 @@ const EmojiInput = {
if (!panel) return if (!panel) return
const picker = this.$refs.picker.$el const picker = this.$refs.picker.$el
const panelBody = this.$refs['panel-body'] const panelBody = this.$refs['panel-body']
const { offsetHeight, offsetTop } = this.input const { offsetHeight, offsetTop } = this.input.elm
const offsetBottom = offsetTop + offsetHeight const offsetBottom = offsetTop + offsetHeight
this.setPlacement(panelBody, panel, offsetBottom) this.setPlacement(panelBody, panel, offsetBottom)
@ -497,7 +494,7 @@ const EmojiInput = {
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) { if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
target.style.top = 'auto' target.style.top = 'auto'
target.style.bottom = this.input.offsetHeight + 'px' target.style.bottom = this.input.elm.offsetHeight + 'px'
} }
}, },
overflowsBottom (el) { overflowsBottom (el) {

View file

@ -1,6 +1,5 @@
<template> <template>
<div <div
ref="root"
v-click-outside="onClickOutside" v-click-outside="onClickOutside"
class="emoji-input" class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }" :class="{ 'with-picker': !hideEmojiButton }"

View file

@ -1,4 +1,3 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -58,7 +57,7 @@ const EmojiPicker = {
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
Checkbox Checkbox
}, },
methods: { methods: {
@ -80,7 +79,7 @@ const EmojiPicker = {
}, },
highlight (key) { highlight (key) {
const ref = this.$refs['group-' + key] const ref = this.$refs['group-' + key]
const top = ref.offsetTop const top = ref[0].offsetTop
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key this.activeGroup = key
this.$nextTick(() => { this.$nextTick(() => {
@ -97,7 +96,7 @@ const EmojiPicker = {
} }
}, },
triggerLoadMore (target) { triggerLoadMore (target) {
const ref = this.$refs['group-end-custom'] const ref = this.$refs['group-end-custom'][0]
if (!ref) return if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight const bottom = ref.offsetTop + ref.offsetHeight
@ -120,7 +119,7 @@ const EmojiPicker = {
this.$nextTick(() => { this.$nextTick(() => {
this.emojisView.forEach(group => { this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id] const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) { if (ref[0].offsetTop <= top) {
this.activeGroup = group.id this.activeGroup = group.id
} }
}) })

View file

@ -15,8 +15,18 @@ const Exporter = {
type: String, type: String,
default: 'export.csv' default: 'export.csv'
}, },
exportButtonLabel: { type: String }, exportButtonLabel: {
processingMessage: { type: String } type: String,
default () {
return this.$t('exporter.export')
}
},
processingMessage: {
type: String,
default () {
return this.$t('exporter.processing')
}
}
}, },
data () { data () {
return { return {

View file

@ -7,14 +7,14 @@
spin spin
/> />
<span>{{ processingMessage || $t('exporter.processing') }}</span> <span>{{ processingMessage }}</span>
</div> </div>
<button <button
v-else v-else
class="btn button-default" class="btn button-default"
@click="process" @click="process"
> >
{{ exportButtonLabel || $t('exporter.export') }} {{ exportButtonLabel }}
</button> </button>
</div> </div>
</template> </template>

View file

@ -7,7 +7,10 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template v-slot:content="{close}"> <div
slot="content"
slot-scope="{close}"
>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -117,15 +120,16 @@
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t("user_card.report") }}</span>
</button> </button>
</div> </div>
</template> </div>
<template v-slot:trigger> <span
<button class="button-unstyled popover-trigger"> slot="trigger"
<FAIcon class="popover-trigger"
class="fa-scale-110 fa-old-padding" >
icon="ellipsis-h" <FAIcon
/> class="fa-scale-110 fa-old-padding"
</button> icon="ellipsis-h"
</template> />
</span>
</Popover> </Popover>
</template> </template>

View file

@ -2,7 +2,7 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
const FeaturesPanel = { const FeaturesPanel = {
computed: { computed: {
shout: function () { return this.$store.state.instance.shoutAvailable }, chat: function () { return this.$store.state.instance.chatAvailable },
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable }, pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
gopher: function () { return this.$store.state.instance.gopherAvailable }, gopher: function () { return this.$store.state.instance.gopherAvailable },
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },

View file

@ -8,8 +8,8 @@
</div> </div>
<div class="panel-body features-panel"> <div class="panel-body features-panel">
<ul> <ul>
<li v-if="shout"> <li v-if="chat">
{{ $t('features_panel.shout') }} {{ $t('features_panel.chat') }}
</li> </li>
<li v-if="pleromaChatMessages"> <li v-if="pleromaChatMessages">
{{ $t('features_panel.pleroma_chat_messages') }} {{ $t('features_panel.pleroma_chat_messages') }}

View file

@ -1,53 +0,0 @@
import RuffleService from '../../services/ruffle_service/ruffle_service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faStop,
faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons'
library.add(
faStop,
faExclamationTriangle
)
const Flash = {
props: [ 'src' ],
data () {
return {
player: false, // can be true, "hidden", false. hidden = element exists
loaded: false,
ruffleInstance: null
}
},
methods: {
openPlayer () {
if (this.player) return // prevent double-loading, or re-loading on failure
this.player = 'hidden'
RuffleService.getRuffle().then((ruffle) => {
const player = ruffle.newest().createPlayer()
player.config = {
letterbox: 'on'
}
const container = this.$refs.container
container.appendChild(player)
player.style.width = '100%'
player.style.height = '100%'
player.load(this.src).then(() => {
this.player = true
}).catch((e) => {
console.error('Error loading ruffle', e)
this.player = 'error'
})
this.ruffleInstance = player
this.$emit('playerOpened')
})
},
closePlayer () {
this.ruffleInstance && this.ruffleInstance.remove()
this.player = false
this.$emit('playerClosed')
}
}
}
export default Flash

View file

@ -1,84 +0,0 @@
<template>
<div class="Flash">
<div
v-if="player === true || player === 'hidden'"
ref="container"
class="player"
:class="{ hidden: player === 'hidden' }"
/>
<button
v-if="player !== true"
class="button-unstyled placeholder"
@click="openPlayer"
>
<span
v-if="player === 'hidden'"
class="label"
>
{{ $t('general.loading') }}
</span>
<span
v-if="player === 'error'"
class="label"
>
{{ $t('general.flash_fail') }}
</span>
<span
v-else
class="label"
>
<p>
{{ $t('general.flash_content') }}
</p>
<p>
<FAIcon icon="exclamation-triangle" />
{{ $t('general.flash_security') }}
</p>
</span>
</button>
</div>
</template>
<script src="./flash.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.Flash {
display: inline-block;
width: 100%;
height: 100%;
position: relative;
.player {
height: 100%;
width: 100%;
}
.placeholder {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
color: var(--link);
}
.hider {
top: 0;
}
.label {
text-align: center;
flex: 1 1 0;
line-height: 1.2;
white-space: normal;
word-wrap: normal;
}
.hidden {
display: none;
visibility: 'hidden';
}
}
</style>

View file

@ -1,6 +1,6 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'labelFollowing', 'buttonClass'],
data () { data () {
return { return {
inProgress: false inProgress: false
@ -14,7 +14,7 @@ export default {
if (this.inProgress || this.relationship.following) { if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow') return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) { } else if (this.relationship.requested) {
return this.$t('user_card.follow_cancel') return this.$t('user_card.follow_again')
} else { } else {
return this.$t('user_card.follow') return this.$t('user_card.follow')
} }
@ -29,14 +29,11 @@ export default {
} else { } else {
return this.$t('user_card.follow') return this.$t('user_card.follow')
} }
},
disabled () {
return this.inProgress || this.user.deactivated
} }
}, },
methods: { methods: {
onClick () { onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following ? this.unfollow() : this.follow()
}, },
follow () { follow () {
this.inProgress = true this.inProgress = true

View file

@ -2,7 +2,7 @@
<button <button
class="btn button-default follow-button" class="btn button-default follow-button"
:class="{ toggled: isPressed }" :class="{ toggled: isPressed }"
:disabled="disabled" :disabled="inProgress"
:title="title" :title="title"
@click="onClick" @click="onClick"
> >

View file

@ -20,7 +20,6 @@
:relationship="relationship" :relationship="relationship"
:label-following="$t('user_card.follow_unfollow')" :label-following="$t('user_card.follow_unfollow')"
class="follow-card-follow-button" class="follow-card-follow-button"
:user="user"
/> />
</template> </template>
</div> </div>

View file

@ -1,17 +1,20 @@
import { set } from 'lodash' import { set } from 'vue'
import Select from '../select/select.vue' import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown
)
export default { export default {
components: {
Select
},
props: [ props: [
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' 'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
], ],
emits: ['update:modelValue'],
data () { data () {
return { return {
lValue: this.modelValue, lValue: this.value,
availableOptions: [ availableOptions: [
this.noInherit ? '' : 'inherit', this.noInherit ? '' : 'inherit',
'custom', 'custom',
@ -23,7 +26,7 @@ export default {
} }
}, },
beforeUpdate () { beforeUpdate () {
this.lValue = this.modelValue this.lValue = this.value
}, },
computed: { computed: {
present () { present () {
@ -38,7 +41,7 @@ export default {
}, },
set (v) { set (v) {
set(this.lValue, 'family', v) set(this.lValue, 'family', v)
this.$emit('update:modelValue', this.lValue) this.$emit('input', this.lValue)
} }
}, },
isCustom () { isCustom () {

View file

@ -15,28 +15,37 @@
class="opt exlcude-disabled" class="opt exlcude-disabled"
type="checkbox" type="checkbox"
:checked="present" :checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
> >
<label <label
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
class="opt-l" class="opt-l"
:for="name + '-o'" :for="name + '-o'"
/> />
{{ ' ' }} <label
<Select :for="name + '-font-switcher'"
:id="name + '-font-switcher'" class="select"
v-model="preset"
:disabled="!present" :disabled="!present"
class="font-switcher"
> >
<option <select
v-for="option in availableOptions" :id="name + '-font-switcher'"
:key="option" v-model="preset"
:value="option" :disabled="!present"
class="font-switcher"
> >
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} <option
</option> v-for="option in availableOptions"
</Select> :key="option"
:value="option"
>
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
<input <input
v-if="isCustom" v-if="isCustom"
:id="name" :id="name"
@ -56,8 +65,7 @@
min-width: 10em; min-width: 10em;
} }
&.custom { &.custom {
/* TODO Should make proper joiners... */ .select {
.font-switcher {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }

View file

@ -1,26 +1,15 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { sumBy, set } from 'lodash' import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = { const Gallery = {
props: [ props: [
'attachments', 'attachments',
'limitRows',
'descriptions',
'limit',
'nsfw', 'nsfw',
'setMedia', 'setMedia'
'size',
'editable',
'removeAttachment',
'shiftUpAttachment',
'shiftDnAttachment',
'editAttachment',
'grid'
], ],
data () { data () {
return { return {
sizes: {}, sizes: {}
hidingLong: true
} }
}, },
components: { Attachment }, components: { Attachment },
@ -29,70 +18,26 @@ const Gallery = {
if (!this.attachments) { if (!this.attachments) {
return [] return []
} }
const attachments = this.limit > 0 const rows = chunk(this.attachments, 3)
? this.attachments.slice(0, this.limit) if (last(rows).length === 1 && rows.length > 1) {
: this.attachments // if 1 attachment on last row -> add it to the previous row instead
if (this.size === 'hide') { const lastAttachment = last(rows)[0]
return attachments.map(item => ({ minimal: true, items: [item] })) const allButLastRow = dropRight(rows)
last(allButLastRow).push(lastAttachment)
return allButLastRow
} }
const rows = this.grid
? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
}
if (!(
attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash')
)) {
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
return [...acc, { items: [] }]
} else {
return acc
}
}, [{ items: [] }]).filter(_ => _.items.length > 0)
return rows return rows
}, },
attachmentsDimensionalScore () { useContainFit () {
return this.rows.reduce((acc, row) => { return this.$store.getters.mergedConfig.useContainFit
let size = 0
if (row.minimal) {
size += 1 / 8
} else if (row.audio) {
size += 1 / 4
} else {
size += 1 / (row.items.length + 0.6)
}
return acc + size
}, 0)
},
tooManyAttachments () {
if (this.editable || this.size === 'small') {
return false
} else if (this.size === 'hide') {
return this.attachments.length > 8
} else {
return this.attachmentsDimensionalScore > 1
}
} }
}, },
methods: { methods: {
onNaturalSizeLoad ({ id, width, height }) { onNaturalSizeLoad (id, size) {
set(this.sizes, id, { width, height }) this.$set(this.sizes, id, size)
}, },
rowStyle (row) { rowStyle (itemsPerRow) {
if (row.audio) { return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
}
}, },
itemStyle (id, row) { itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id)) const total = sumBy(row, item => this.getAspectRatio(item.id))
@ -101,16 +46,6 @@ const Gallery = {
getAspectRatio (id) { getAspectRatio (id) {
const size = this.sizes[id] const size = this.sizes[id]
return size ? size.width / size.height : 1 return size ? size.width / size.height : 1
},
toggleHidingLong (event) {
this.hidingLong = event
},
openGallery () {
this.$store.dispatch('setMedia', this.attachments)
this.$store.dispatch('setCurrentMedia', this.attachments[0])
},
onMedia () {
this.$store.dispatch('setMedia', this.attachments)
} }
} }
} }

View file

@ -1,83 +1,26 @@
<template> <template>
<div <div
ref="galleryContainer" ref="galleryContainer"
class="Gallery" style="width: 100%;"
:class="{ '-long': tooManyAttachments && hidingLong }"
> >
<div class="gallery-rows">
<div
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="gallery-row"
:style="rowStyle(row)"
:class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }"
>
<div
class="gallery-row-inner"
:class="{ '-grid': grid }"
>
<Attachment
v-for="(attachment, attachmentIndex) in row.items"
:key="attachment.id"
class="gallery-item"
:nsfw="nsfw"
:attachment="attachment"
:size="size"
:editable="editable"
:remove="removeAttachment"
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
:edit="editAttachment"
:description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
:style="itemStyle(attachment.id, row.items)"
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
</div>
</div>
</div>
<div <div
v-if="tooManyAttachments" v-for="(row, index) in rows"
class="many-attachments" :key="index"
class="gallery-row"
:style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
> >
<div class="many-attachments-text"> <div class="gallery-row-inner">
{{ $t("status.many_attachments", { number: attachments.length }) }} <attachment
</div> v-for="attachment in row"
<div class="many-attachments-buttons"> :key="attachment.id"
<span :set-media="setMedia"
v-if="!hidingLong" :nsfw="nsfw"
class="many-attachments-button" :attachment="attachment"
> :allow-play="false"
<button :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
class="button-unstyled -link" :style="itemStyle(attachment.id, row)"
@click="toggleHidingLong(true)" />
>
{{ $t("status.collapse_attachments") }}
</button>
</span>
<span
v-if="hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="toggleHidingLong(false)"
>
{{ $t("status.show_all_attachments") }}
</button>
</span>
<span
v-if="hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="openGallery"
>
{{ $t("status.open_gallery") }}
</button>
</span>
</div> </div>
</div> </div>
</div> </div>
@ -88,66 +31,12 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.Gallery { .gallery-row {
.gallery-rows { position: relative;
display: flex; height: 0;
flex-direction: column; width: 100%;
} flex-grow: 1;
margin-top: 0.5em;
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
&:not(:first-child) {
margin-top: 0.5em;
}
}
&.-long {
.gallery-rows {
max-height: 25em;
overflow: hidden;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
.many-attachments-text {
text-align: center;
line-height: 2;
}
.many-attachments-buttons {
display: flex;
}
.many-attachments-button {
display: flex;
flex: 1;
justify-content: center;
line-height: 2;
button {
padding: 0 2em;
}
}
.gallery-row {
&.-grid,
&.-minimal {
height: auto;
.gallery-row-inner {
position: relative;
}
}
}
.gallery-row-inner { .gallery-row-inner {
position: absolute; position: absolute;
@ -159,24 +48,9 @@
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-content: stretch; align-content: stretch;
&.-grid {
width: 100%;
height: auto;
position: relative;
display: grid;
grid-column-gap: 0.5em;
grid-row-gap: 0.5em;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
.gallery-item {
margin: 0;
height: 200px;
}
}
} }
.gallery-item { .gallery-row-inner .attachment {
margin: 0 0.5em 0 0; margin: 0 0.5em 0 0;
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;
@ -187,5 +61,32 @@
margin: 0; margin: 0;
} }
} }
.image-attachment {
width: 100%;
height: 100%;
}
.video-container {
height: 100%;
}
&.contain-fit {
img,
video,
canvas {
object-fit: contain;
height: 100%;
}
}
&.cover-fit {
img,
video,
canvas {
object-fit: cover;
}
}
} }
</style> </style>

View file

@ -1,36 +0,0 @@
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
const HashtagLink = {
name: 'HashtagLink',
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
tag: {
required: false,
type: String,
default: ''
}
},
methods: {
onClick () {
const tag = this.tag || extractTagFromUrl(this.url)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
} else {
window.open(this.url, '_blank')
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default HashtagLink

View file

@ -1,6 +0,0 @@
.HashtagLink {
position: relative;
white-space: normal;
display: inline-block;
color: var(--link);
}

View file

@ -1,19 +0,0 @@
<template>
<span
class="HashtagLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
:href="url"
class="original"
target="_blank"
@click.prevent="onClick"
v-html="content"
/>
<!-- eslint-enable vue/no-v-html -->
</span>
</template>
<script src="./hashtag_link.js"/>
<style lang="scss" src="./hashtag_link.scss"/>

View file

@ -117,7 +117,7 @@ const ImageCropper = {
const fileInput = this.$refs.input const fileInput = this.$refs.input
fileInput.addEventListener('change', this.readFile) fileInput.addEventListener('change', this.readFile)
}, },
beforeUnmount: function () { beforeDestroy: function () {
// remove the event listeners // remove the event listeners
const trigger = this.getTriggerDOM() const trigger = this.getTriggerDOM()
if (trigger) { if (trigger) {

View file

@ -15,9 +15,24 @@ const Importer = {
type: Function, type: Function,
required: true required: true
}, },
submitButtonLabel: { type: String }, submitButtonLabel: {
successMessage: { type: String }, type: String,
errorMessage: { type: String } default () {
return this.$t('importer.submit')
}
},
successMessage: {
type: String,
default () {
return this.$t('importer.success')
}
},
errorMessage: {
type: String,
default () {
return this.$t('importer.error')
}
}
}, },
data () { data () {
return { return {

View file

@ -18,31 +18,21 @@
class="btn button-default" class="btn button-default"
@click="submit" @click="submit"
> >
{{ submitButtonLabel || $t('importer.submit') }} {{ submitButtonLabel }}
</button> </button>
<div v-if="success"> <div v-if="success">
<button <FAIcon
class="button-unstyled" icon="times"
@click="dismiss" @click="dismiss"
> />
<FAIcon <p>{{ successMessage }}</p>
icon="times"
/>
</button>
{{ ' ' }}
<span>{{ successMessage || $t('importer.success') }}</span>
</div> </div>
<div v-else-if="error"> <div v-else-if="error">
<button <FAIcon
class="button-unstyled" icon="times"
@click="dismiss" @click="dismiss"
> />
<FAIcon <p>{{ errorMessage }}</p>
icon="times"
/>
</button>
{{ ' ' }}
<span>{{ errorMessage || $t('importer.error') }}</span>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,5 +1,4 @@
import Notifications from '../notifications/notifications.vue' import Notifications from '../notifications/notifications.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
const tabModeDict = { const tabModeDict = {
mentions: ['mention'], mentions: ['mention'],
@ -21,8 +20,7 @@ const Interactions = {
} }
}, },
components: { components: {
Notifications, Notifications
TabSwitcher
} }
} }

View file

@ -3,19 +3,27 @@
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }} {{ $t('settings.interfaceLanguage') }}
</label> </label>
{{ ' ' }} <label
<Select for="interface-language-switcher"
id="interface-language-switcher" class="select"
v-model="language"
> >
<option <select
v-for="lang in languages" id="interface-language-switcher"
:key="lang.code" v-model="language"
:value="lang.code"
> >
{{ lang.name }} <option
</option> v-for="lang in languages"
</Select> :key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div> </div>
</template> </template>
@ -24,12 +32,16 @@ import languagesObject from '../../i18n/messages'
import localeService from '../../services/locale/locale.service.js' import localeService from '../../services/locale/locale.service.js'
import ISO6391 from 'iso-639-1' import ISO6391 from 'iso-639-1'
import _ from 'lodash' import _ from 'lodash'
import Select from '../select/select.vue' import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown
)
export default { export default {
components: {
Select
},
computed: { computed: {
languages () { languages () {
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))

View file

@ -76,15 +76,11 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<button <FAIcon
class="button-unstyled" class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
> />
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,46 +1,24 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue' import Modal from '../modal/modal.vue'
import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
import SwipeClick from '../swipe_click/swipe_click.vue'
import GestureService from '../../services/gesture_service/gesture_service'
import Flash from 'src/components/flash/flash.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import GestureService from '../../services/gesture_service/gesture_service'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight
faCircleNotch,
faTimes
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight
faCircleNotch,
faTimes
) )
const MediaModal = { const MediaModal = {
components: { components: {
StillImage, StillImage,
VideoAttachment, VideoAttachment,
PinchZoom, Modal
SwipeClick,
Modal,
Flash
},
data () {
return {
loading: false,
swipeDirection: GestureService.DIRECTION_LEFT,
swipeThreshold: () => {
const considerableMoveRatio = 1 / 4
return window.innerWidth * considerableMoveRatio
},
pinchZoomMinScale: 1,
pinchZoomScaleResetLimit: 1.2
}
}, },
computed: { computed: {
showing () { showing () {
@ -49,9 +27,6 @@ const MediaModal = {
media () { media () {
return this.$store.state.mediaViewer.media return this.$store.state.mediaViewer.media
}, },
description () {
return this.currentMedia.description
},
currentIndex () { currentIndex () {
return this.$store.state.mediaViewer.currentIndex return this.$store.state.mediaViewer.currentIndex
}, },
@ -62,62 +37,43 @@ const MediaModal = {
return this.media.length > 1 return this.media.length > 1
}, },
type () { type () {
return this.currentMedia ? this.getType(this.currentMedia) : null return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
} }
}, },
created () {
this.mediaSwipeGestureRight = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT,
this.goPrev,
50
)
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
GestureService.DIRECTION_LEFT,
this.goNext,
50
)
},
methods: { methods: {
getType (media) { mediaTouchStart (e) {
return fileTypeService.fileType(media.mimetype) GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
},
mediaTouchMove (e) {
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
}, },
hide () { hide () {
// HACK: Closing immediately via a touch will cause the click this.$store.dispatch('closeMediaViewer')
// to be processed on the content below the overlay
const transitionTime = 100 // ms
setTimeout(() => {
this.$store.dispatch('closeMediaViewer')
}, transitionTime)
},
hideIfNotSwiped (event) {
// If we have swiped over SwipeClick, do not trigger hide
const comp = this.$refs.swipeClick
if (!comp) {
this.hide()
} else {
comp.$gesture.click(event)
}
}, },
goPrev () { goPrev () {
if (this.canNavigate) { if (this.canNavigate) {
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
const newMedia = this.media[prevIndex] this.$store.dispatch('setCurrent', this.media[prevIndex])
if (this.getType(newMedia) === 'image') {
this.loading = true
}
this.$store.dispatch('setCurrentMedia', newMedia)
} }
}, },
goNext () { goNext () {
if (this.canNavigate) { if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
const newMedia = this.media[nextIndex] this.$store.dispatch('setCurrent', this.media[nextIndex])
if (this.getType(newMedia) === 'image') {
this.loading = true
}
this.$store.dispatch('setCurrentMedia', newMedia)
}
},
onImageLoaded () {
this.loading = false
},
handleSwipePreview (offsets) {
this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
},
handleSwipeEnd (sign) {
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
if (sign > 0) {
this.goNext()
} else if (sign < 0) {
this.goPrev()
} }
}, },
handleKeyupEvent (e) { handleKeyupEvent (e) {
@ -142,7 +98,7 @@ const MediaModal = {
document.addEventListener('keyup', this.handleKeyupEvent) document.addEventListener('keyup', this.handleKeyupEvent)
document.addEventListener('keydown', this.handleKeydownEvent) document.addEventListener('keydown', this.handleKeydownEvent)
}, },
unmounted () { destroyed () {
window.removeEventListener('popstate', this.hide) window.removeEventListener('popstate', this.hide)
document.removeEventListener('keyup', this.handleKeyupEvent) document.removeEventListener('keyup', this.handleKeyupEvent)
document.removeEventListener('keydown', this.handleKeydownEvent) document.removeEventListener('keydown', this.handleKeydownEvent)

View file

@ -2,38 +2,18 @@
<Modal <Modal
v-if="showing" v-if="showing"
class="media-modal-view" class="media-modal-view"
@backdropClicked="hideIfNotSwiped" @backdropClicked="hide"
> >
<SwipeClick <img
v-if="type === 'image'" v-if="type === 'image'"
ref="swipeClick" class="modal-image"
class="modal-image-container" :src="currentMedia.url"
:direction="swipeDirection" :alt="currentMedia.description"
:threshold="swipeThreshold" :title="currentMedia.description"
@preview-requested="handleSwipePreview" @touchstart.stop="mediaTouchStart"
@swipe-finished="handleSwipeEnd" @touchmove.stop="mediaTouchMove"
@swipeless-clicked="hide" @click="hide"
> >
<PinchZoom
ref="pinchZoom"
class="modal-image-container-inner"
selector=".modal-image"
reach-min-scale-strategy="reset"
stop-propagate-handled="stop-propgate-handled"
:allow-pan-min-scale="pinchZoomMinScale"
:min-scale="pinchZoomMinScale"
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
>
<img
:class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
@load="onImageLoaded"
>
</PinchZoom>
</SwipeClick>
<VideoAttachment <VideoAttachment
v-if="type === 'video'" v-if="type === 'video'"
class="modal-image" class="modal-image"
@ -48,84 +28,38 @@
:title="currentMedia.description" :title="currentMedia.description"
controls controls
/> />
<Flash
v-if="type === 'flash'"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
/>
<button <button
v-if="canNavigate" v-if="canNavigate"
:title="$t('media_modal.previous')" :title="$t('media_modal.previous')"
class="modal-view-button modal-view-button-arrow modal-view-button-arrow--prev" class="modal-view-button-arrow modal-view-button-arrow--prev"
@click.stop.prevent="goPrev" @click.stop.prevent="goPrev"
> >
<FAIcon <FAIcon
class="button-icon arrow-icon" class="arrow-icon"
icon="chevron-left" icon="chevron-left"
/> />
</button> </button>
<button <button
v-if="canNavigate" v-if="canNavigate"
:title="$t('media_modal.next')" :title="$t('media_modal.next')"
class="modal-view-button modal-view-button-arrow modal-view-button-arrow--next" class="modal-view-button-arrow modal-view-button-arrow--next"
@click.stop.prevent="goNext" @click.stop.prevent="goNext"
> >
<FAIcon <FAIcon
class="button-icon arrow-icon" class="arrow-icon"
icon="chevron-right" icon="chevron-right"
/> />
</button> </button>
<button
class="modal-view-button modal-view-button-hide"
:title="$t('media_modal.hide')"
@click.stop.prevent="hide"
>
<FAIcon
class="button-icon"
icon="times"
/>
</button>
<span
v-if="description"
class="description"
>
{{ description }}
</span>
<span
class="counter"
>
{{ $tc('media_modal.counter', currentIndex + 1, { current: currentIndex + 1, total: media.length }) }}
</span>
<span
v-if="loading"
class="loading-spinner"
>
<FAIcon
spin
icon="circle-notch"
size="5x"
/>
</span>
</Modal> </Modal>
</template> </template>
<script src="./media_modal.js"></script> <script src="./media_modal.js"></script>
<style lang="scss"> <style lang="scss">
$modal-view-button-icon-height: 3em;
$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2);
$modal-view-button-icon-width: 3em;
$modal-view-button-icon-margin: 0.5em;
.modal-view.media-modal-view { .modal-view.media-modal-view {
z-index: 1001; z-index: 1001;
flex-direction: column;
.modal-view-button-arrow, .modal-view-button-arrow {
.modal-view-button-hide {
opacity: 0.75; opacity: 0.75;
&:focus, &:focus,
@ -133,154 +67,69 @@ $modal-view-button-icon-margin: 0.5em;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
} }
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
overflow: hidden;
} }
.media-modal-view { @keyframes media-fadein {
@keyframes media-fadein { from {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-image-container {
display: flex;
overflow: hidden;
align-items: center;
flex-direction: column;
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
flex-grow: 1;
justify-content: center;
&-inner {
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.description,
.counter {
/* Hardcoded since background is also hardcoded */
color: white;
margin-top: 1em;
text-shadow: 0 0 10px black, 0 0 10px black;
padding: 0.2em 2em;
}
.description {
flex: 0 0 auto;
overflow-y: auto;
min-height: 1em;
max-width: 500px;
max-height: 9.5em;
word-break: break-all;
}
.modal-image {
max-width: 100%;
max-height: 100%;
image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
&.loading {
opacity: 0.5;
}
}
.loading-spinner {
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
svg {
color: white;
}
}
.modal-view-button {
border: 0;
padding: 0;
opacity: 0; opacity: 0;
box-shadow: none; }
background: none; to {
appearance: none; opacity: 1;
overflow: visible; }
cursor: pointer; }
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
height: $modal-view-button-icon-height;
width: $modal-view-button-icon-width;
.button-icon { .modal-image {
position: absolute; max-width: 90%;
height: $modal-view-button-icon-height; max-height: 90%;
width: $modal-view-button-icon-width; box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
font-size: 14px; image-orientation: from-image; // NOTE: only FF supports this
line-height: $modal-view-button-icon-height; animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
color: #FFF; }
text-align: center;
background-color: rgba(0,0,0,.3); .modal-view-button-arrow {
} position: absolute;
display: block;
top: 50%;
margin-top: -50px;
width: 70px;
height: 100px;
border: 0;
padding: 0;
opacity: 0;
box-shadow: none;
background: none;
appearance: none;
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
.arrow-icon {
position: absolute;
top: 35px;
height: 30px;
width: 32px;
font-size: 14px;
line-height: 30px;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
} }
.modal-view-button-arrow { &--prev {
position: absolute; left: 0;
display: block;
top: 50%;
margin-top: $modal-view-button-icon-half-height;
width: $modal-view-button-icon-width;
height: $modal-view-button-icon-height;
.arrow-icon { .arrow-icon {
position: absolute; left: 6px;
top: 0;
line-height: $modal-view-button-icon-height;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
}
&--prev {
left: 0;
.arrow-icon {
left: $modal-view-button-icon-margin;
}
}
&--next {
right: 0;
.arrow-icon {
right: $modal-view-button-icon-margin;
}
} }
} }
.modal-view-button-hide { &--next {
position: absolute;
top: 0;
right: 0; right: 0;
.button-icon { .arrow-icon {
top: $modal-view-button-icon-margin; right: 6px;
right: $modal-view-button-icon-margin;
} }
} }
} }

View file

@ -1,134 +0,0 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAt
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAt
)
const MentionLink = {
name: 'MentionLink',
components: {
UserAvatar
},
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
userId: {
required: false,
type: String
},
userScreenName: {
required: false,
type: String
}
},
methods: {
onClick () {
const link = generateProfileLink(
this.userId || this.user.id,
this.userScreenName || this.user.screen_name
)
this.$router.push(link)
}
},
computed: {
user () {
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
},
isYou () {
// FIXME why user !== currentUser???
return this.user && this.user.id === this.currentUser.id
},
userName () {
return this.user && this.userNameFullUi.split('@')[0]
},
serverName () {
// XXX assumed that domain does not contain @
return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain)
},
userNameFull () {
return this.user && this.user.screen_name
},
userNameFullUi () {
return this.user && this.user.screen_name_ui
},
highlight () {
return this.user && this.mergedConfig.highlight[this.user.screen_name]
},
highlightType () {
return this.highlight && ('-' + this.highlight.type)
},
highlightClass () {
if (this.highlight) return highlightClass(this.user)
},
style () {
if (this.highlight) {
const {
backgroundColor,
backgroundPosition,
backgroundImage,
...rest
} = highlightStyle(this.highlight)
return rest
}
},
classnames () {
return [
{
'-you': this.isYou && this.shouldBoldenYou,
'-highlighted': this.highlight
},
this.highlightType
]
},
useAtIcon () {
return this.mergedConfig.useAtIcon
},
isRemote () {
return this.userName !== this.userNameFull
},
shouldShowFullUserName () {
const conf = this.mergedConfig.mentionLinkDisplay
if (conf === 'short') {
return false
} else if (conf === 'full') {
return true
} else { // full_for_remote
return this.isRemote
}
},
shouldShowTooltip () {
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
},
shouldShowAvatar () {
return this.mergedConfig.mentionLinkShowAvatar
},
shouldShowYous () {
return this.mergedConfig.mentionLinkShowYous
},
shouldBoldenYou () {
return this.mergedConfig.mentionLinkBoldenYou
},
shouldFadeDomain () {
return this.mergedConfig.mentionLinkFadeDomain
},
...mapGetters(['mergedConfig']),
...mapState({
currentUser: state => state.users.currentUser
})
}
}
export default MentionLink

View file

@ -1,115 +0,0 @@
@import '../../_variables.scss';
.MentionLink {
position: relative;
white-space: normal;
display: inline;
color: var(--link);
word-break: normal;
& .new,
& .original {
display: inline;
border-radius: 2px;
}
.mention-avatar {
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
width: 1.5em;
height: 1.5em;
vertical-align: middle;
user-select: none;
margin-right: 0.2em;
}
.full {
position: absolute;
display: inline-block;
pointer-events: none;
opacity: 0;
top: 100%;
left: 0;
height: 100%;
word-wrap: normal;
white-space: nowrap;
transition: opacity 0.2s ease;
z-index: 1;
margin-top: 0.25em;
padding: 0.5em;
user-select: all;
}
& .short.-with-tooltip,
& .you {
user-select: none;
}
& .short,
& .full {
white-space: nowrap;
}
.shortName {
white-space: normal;
}
.new {
&.-you {
& .shortName,
& .full {
font-weight: 600;
}
}
.at {
color: var(--link);
opacity: 0.8;
display: inline-block;
line-height: 1;
padding: 0 0.1em;
vertical-align: -25%;
margin: 0;
}
&.-striped {
& .shortName,
& .full {
background-image:
repeating-linear-gradient(
135deg,
var(--____highlight-tintColor),
var(--____highlight-tintColor) 5px,
var(--____highlight-tintColor2) 5px,
var(--____highlight-tintColor2) 10px
);
}
}
&.-solid {
& .shortName,
& .full {
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
}
}
&.-side {
& .shortName,
& .userNameFull {
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
}
}
}
&:hover .new .full {
opacity: 1;
pointer-events: initial;
}
.serverName.-faded {
color: var(--faintLink, $fallback--link);
}
.full .-faded {
color: var(--faint, $fallback--faint);
}
}

View file

@ -1,77 +0,0 @@
<template>
<span
class="MentionLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
v-if="!user"
:href="url"
class="original"
target="_blank"
v-html="content"
/><!-- eslint-enable vue/no-v-html --><span
v-if="user"
class="new"
:style="style"
:class="classnames"
>
<a
class="short button-unstyled"
:class="{ '-with-tooltip': shouldShowTooltip }"
:href="url"
@click.prevent="onClick"
>
<!-- eslint-disable vue/no-v-html -->
<UserAvatar
v-if="shouldShowAvatar"
class="mention-avatar"
:user="user"
/><span
class="shortName"
><FAIcon
v-if="useAtIcon"
size="sm"
icon="at"
class="at"
/>{{ !useAtIcon ? '@' : '' }}<span
class="userName"
v-html="userName"
/><span
v-if="shouldShowFullUserName"
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
/>
</span>
<span
v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }"
> {{ ' ' + $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a><span
v-if="shouldShowTooltip"
class="full popover-default"
:class="[highlightType]"
>
<span
class="userNameFull"
>
<!-- eslint-disable vue/no-v-html -->
@<span
class="userName"
v-html="userName"
/><span
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
/>
<!-- eslint-enable vue/no-v-html -->
</span>
</span>
</span>
</span>
</template>
<script src="./mention_link.js"/>
<style lang="scss" src="./mention_link.scss"/>

View file

@ -1,37 +0,0 @@
import MentionLink from 'src/components/mention_link/mention_link.vue'
import { mapGetters } from 'vuex'
export const MENTIONS_LIMIT = 5
const MentionsLine = {
name: 'MentionsLine',
props: {
mentions: {
required: true,
type: Array
}
},
data: () => ({ expanded: false }),
components: {
MentionLink
},
computed: {
mentionsComputed () {
return this.mentions.slice(0, MENTIONS_LIMIT)
},
extraMentions () {
return this.mentions.slice(MENTIONS_LIMIT)
},
manyMentions () {
return this.extraMentions.length > 0
},
...mapGetters(['mergedConfig'])
},
methods: {
toggleShowMore () {
this.expanded = !this.expanded
}
}
}
export default MentionsLine

View file

@ -1,13 +0,0 @@
.MentionsLine {
word-break: break-all;
.mention-link:not(:first-child)::before {
content: ' ';
}
.showMoreLess {
margin-left: 0.5em;
white-space: normal;
color: var(--link);
}
}

View file

@ -1,41 +0,0 @@
<template>
<span class="MentionsLine">
<MentionLink
v-for="mention in mentionsComputed"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
/><span
v-if="manyMentions"
class="extraMentions"
>
<span
v-if="expanded"
class="fullExtraMentions"
>
<MentionLink
v-for="mention in extraMentions"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
/>
</span><button
v-if="!expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('status.plus_more', { number: extraMentions.length }) }}
</button><button
v-if="expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('general.show_less') }}
</button>
</span>
</span>
</template>
<script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" />

View file

@ -56,15 +56,11 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<button <FAIcon
class="button-unstyled" class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
> />
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -58,16 +58,12 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<button <FAIcon
class="button-unstyled" size="lg"
class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
> />
<FAIcon
size="lg"
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -99,9 +99,6 @@
width: 100%; width: 100%;
position: fixed; position: fixed;
box-sizing: border-box; box-sizing: border-box;
a {
color: var(--topBarLink, $fallback--link);
}
} }
.mobile-inner-nav { .mobile-inner-nav {

View file

@ -29,7 +29,7 @@ const MobilePostStatusButton = {
} }
window.addEventListener('resize', this.handleOSK) window.addEventListener('resize', this.handleOSK)
}, },
unmounted () { destroyed () {
if (this.autohideFloatingPostButton) { if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide() this.deactivateFloatingPostButtonAutohide()
} }
@ -44,9 +44,6 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive) return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
}, },
isPersistent () {
return !!this.$store.getters.mergedConfig.showNewPostButton
},
autohideFloatingPostButton () { autohideFloatingPostButton () {
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
} }

View file

@ -1,12 +1,13 @@
<template> <template>
<button <div v-if="isLoggedIn">
v-if="isLoggedIn" <button
class="MobilePostButton button-default new-status-button" class="button-default new-status-button"
:class="{ 'hidden': isHidden, 'always-show': isPersistent }" :class="{ 'hidden': isHidden }"
@click="openPostForm" @click="openPostForm"
> >
<FAIcon icon="pen" /> <FAIcon icon="pen" />
</button> </button>
</div>
</template> </template>
<script src="./mobile_post_status_button.js"></script> <script src="./mobile_post_status_button.js"></script>
@ -14,27 +15,25 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.MobilePostButton { .new-status-button {
&.button-default { width: 5em;
width: 5em; height: 5em;
height: 5em; border-radius: 100%;
border-radius: 100%; position: fixed;
position: fixed; bottom: 1.5em;
bottom: 1.5em; right: 1.5em;
right: 1.5em; // TODO: this needs its own color, it has to stand out enough and link color
// TODO: this needs its own color, it has to stand out enough and link color // is not very optimal for this particular use.
// is not very optimal for this particular use. background-color: $fallback--fg;
background-color: $fallback--fg; background-color: var(--btn, $fallback--fg);
background-color: var(--btn, $fallback--fg); display: flex;
display: flex; justify-content: center;
justify-content: center; align-items: center;
align-items: center; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3); z-index: 10;
z-index: 10;
transition: 0.35s transform; transition: 0.35s transform;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1); transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
&.hidden { &.hidden {
transform: translateY(150%); transform: translateY(150%);
@ -48,7 +47,7 @@
} }
@media all and (min-width: 801px) { @media all and (min-width: 801px) {
.new-status-button:not(.always-show) { .new-status-button {
display: none; display: none;
} }
} }

View file

@ -8,7 +8,7 @@
@show="setToggled(true)" @show="setToggled(true)"
@close="setToggled(false)" @close="setToggled(false)"
> >
<template v-slot:content> <div slot="content">
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
@ -121,27 +121,26 @@
</button> </button>
</span> </span>
</div> </div>
</template> </div>
<template v-slot:trigger> <button
<button slot="trigger"
class="btn button-default btn-block moderation-tools-button" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled }"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" /> <FAIcon icon="chevron-down" />
</button> </button>
</template>
</Popover> </Popover>
<teleport to="#modal"> <portal to="modal">
<DialogModal <DialogModal
v-if="showDeleteUserDialog" v-if="showDeleteUserDialog"
:on-cancel="deleteUserDialog.bind(this, false)" :on-cancel="deleteUserDialog.bind(this, false)"
> >
<template v-slot:header> <template slot="header">
{{ $t('user_card.admin_menu.delete_user') }} {{ $t('user_card.admin_menu.delete_user') }}
</template> </template>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template v-slot:footer> <template slot="footer">
<button <button
class="btn button-default" class="btn button-default"
@click="deleteUserDialog(false)" @click="deleteUserDialog(false)"
@ -156,7 +155,7 @@
</button> </button>
</template> </template>
</DialogModal> </DialogModal>
</teleport> </portal>
</div> </div>
</template> </template>

View file

@ -1,56 +1,17 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { get } from 'lodash' import { get } from 'lodash'
/**
* This is for backwards compatibility. We originally didn't recieve
* extra info like a reason why an instance was rejected/quarantined/etc.
* Because we didn't want to break backwards compatibility it was decided
* to add an extra "info" key.
*/
const toInstanceReasonObject = (instances, info, key) => {
return instances.map(instance => {
if (info[key] && info[key][instance] && info[key][instance]['reason']) {
return { instance: instance, reason: info[key][instance]['reason'] }
}
return { instance: instance, reason: '' }
})
}
const MRFTransparencyPanel = { const MRFTransparencyPanel = {
computed: { computed: {
...mapState({ ...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'), federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []), mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => toInstanceReasonObject( quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
get(state, 'instance.federationPolicy.quarantined_instances', []), acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
get(state, 'instance.federationPolicy.quarantined_instances_info', []), rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
'quarantined_instances' ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
), mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
acceptInstances: state => toInstanceReasonObject( mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'accept'
),
rejectInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'reject'
),
ftlRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'federated_timeline_removal'
),
mediaNsfwInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_nsfw'
),
mediaRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_removal'
),
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []), keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []), keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', []) keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])

View file

@ -1,21 +0,0 @@
.mrf-section {
margin: 1em;
table {
width:100%;
text-align: left;
padding-left:10px;
padding-bottom:20px;
th, td {
width: 180px;
max-width: 360px;
overflow: hidden;
vertical-align: text-top;
}
th+th, td+td {
width: auto;
}
}
}

View file

@ -31,24 +31,13 @@
<p>{{ $t("about.mrf.simple.accept_desc") }}</p> <p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in acceptInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in acceptInstances" </ul>
:key="entry.instance + '_accept'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="rejectInstances.length"> <div v-if="rejectInstances.length">
@ -56,24 +45,13 @@
<p>{{ $t("about.mrf.simple.reject_desc") }}</p> <p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in rejectInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in rejectInstances" </ul>
:key="entry.instance + '_reject'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="quarantineInstances.length"> <div v-if="quarantineInstances.length">
@ -81,24 +59,13 @@
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p> <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in quarantineInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in quarantineInstances" </ul>
:key="entry.instance + '_quarantine'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="ftlRemovalInstances.length"> <div v-if="ftlRemovalInstances.length">
@ -106,24 +73,13 @@
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p> <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in ftlRemovalInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in ftlRemovalInstances" </ul>
:key="entry.instance + '_ftl_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="mediaNsfwInstances.length"> <div v-if="mediaNsfwInstances.length">
@ -131,24 +87,13 @@
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p> <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in mediaNsfwInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in mediaNsfwInstances" </ul>
:key="entry.instance + '_media_nsfw'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="mediaRemovalInstances.length"> <div v-if="mediaRemovalInstances.length">
@ -156,24 +101,13 @@
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p> <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<table> <ul>
<tr> <li
<th>{{ $t("about.mrf.simple.instance") }}</th> v-for="instance in mediaRemovalInstances"
<th>{{ $t("about.mrf.simple.reason") }}</th> :key="instance"
</tr> v-text="instance"
<tr />
v-for="entry in mediaRemovalInstances" </ul>
:key="entry.instance + '_media_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<h2 v-if="hasKeywordPolicies"> <h2 v-if="hasKeywordPolicies">
@ -227,6 +161,7 @@
<script src="./mrf_transparency_panel.js"></script> <script src="./mrf_transparency_panel.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .mrf-section {
@import './mrf_transparency_panel.scss'; margin: 1em;
}
</style> </style>

View file

@ -4,7 +4,6 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -45,8 +44,7 @@ const Notification = {
UserAvatar, UserAvatar,
UserCard, UserCard,
Timeago, Timeago,
Status, Status
RichContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -2,19 +2,6 @@
// TODO Copypaste from Status, should unify it somehow // TODO Copypaste from Status, should unify it somehow
.Notification { .Notification {
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
word-wrap: break-word;
word-break: break-word;
--emoji-size: 14px;
&:hover {
--_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
}
&.-muted { &.-muted {
padding: 0.25em 0.6em; padding: 0.25em 0.6em;
height: 1.2em; height: 1.2em;

View file

@ -1,7 +1,6 @@
<template> <template>
<Status <Status
v-if="notification.type === 'mention'" v-if="notification.type === 'mention'"
class="Notification"
:compact="true" :compact="true"
:statusoid="notification.status" :statusoid="notification.status"
/> />
@ -33,7 +32,7 @@
> >
<a <a
class="avatar-container" class="avatar-container"
:href="$router.resolve(userProfileLink).href" :href="notification.from_profile.statusnet_profile_url"
@click.stop.prevent.capture="toggleUserExpanded" @click.stop.prevent.capture="toggleUserExpanded"
> >
<UserAvatar <UserAvatar
@ -52,29 +51,23 @@
<span class="notification-details"> <span class="notification-details">
<div class="name-and-action"> <div class="name-and-action">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<bdi v-if="!!notification.from_profile.name_html"> <bdi
<RichContent v-if="!!notification.from_profile.name_html"
class="username" class="username"
:title="'@'+notification.from_profile.screen_name_ui" :title="'@'+notification.from_profile.screen_name_ui"
:html="notification.from_profile.name_html" v-html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji" />
/>
</bdi>
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span <span
v-else v-else
class="username" class="username"
:title="'@'+notification.from_profile.screen_name_ui" :title="'@'+notification.from_profile.screen_name_ui"
> >{{ notification.from_profile.name }}</span>
{{ notification.from_profile.name }}
</span>
{{ ' ' }}
<span v-if="notification.type === 'like'"> <span v-if="notification.type === 'like'">
<FAIcon <FAIcon
class="type-icon" class="type-icon"
icon="star" icon="star"
/> />
{{ ' ' }}
<small>{{ $t('notifications.favorited_you') }}</small> <small>{{ $t('notifications.favorited_you') }}</small>
</span> </span>
<span v-if="notification.type === 'repeat'"> <span v-if="notification.type === 'repeat'">
@ -83,7 +76,6 @@
icon="retweet" icon="retweet"
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
/> />
{{ ' ' }}
<small>{{ $t('notifications.repeated_you') }}</small> <small>{{ $t('notifications.repeated_you') }}</small>
</span> </span>
<span v-if="notification.type === 'follow'"> <span v-if="notification.type === 'follow'">
@ -91,7 +83,6 @@
class="type-icon" class="type-icon"
icon="user-plus" icon="user-plus"
/> />
{{ ' ' }}
<small>{{ $t('notifications.followed_you') }}</small> <small>{{ $t('notifications.followed_you') }}</small>
</span> </span>
<span v-if="notification.type === 'follow_request'"> <span v-if="notification.type === 'follow_request'">
@ -99,7 +90,6 @@
class="type-icon" class="type-icon"
icon="user" icon="user"
/> />
{{ ' ' }}
<small>{{ $t('notifications.follow_request') }}</small> <small>{{ $t('notifications.follow_request') }}</small>
</span> </span>
<span v-if="notification.type === 'move'"> <span v-if="notification.type === 'move'">
@ -107,17 +97,13 @@
class="type-icon" class="type-icon"
icon="suitcase-rolling" icon="suitcase-rolling"
/> />
{{ ' ' }}
<small>{{ $t('notifications.migrated_to') }}</small> <small>{{ $t('notifications.migrated_to') }}</small>
</span> </span>
<span v-if="notification.type === 'pleroma:emoji_reaction'"> <span v-if="notification.type === 'pleroma:emoji_reaction'">
<small> <small>
<i18n-t <i18n path="notifications.reacted_with">
scope="global"
keypath="notifications.reacted_with"
>
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span> <span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
</i18n-t> </i18n>
</small> </small>
</span> </span>
</div> </div>
@ -172,26 +158,18 @@
v-if="notification.type === 'follow_request'" v-if="notification.type === 'follow_request'"
style="white-space: nowrap;" style="white-space: nowrap;"
> >
<button <FAIcon
class="button-unstyled" icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
:title="$t('tool_tip.accept_follow_request')" :title="$t('tool_tip.accept_follow_request')"
@click="approveUser()" @click="approveUser()"
> />
<FAIcon <FAIcon
icon="check" icon="times"
class="fa-scale-110 fa-old-padding follow-request-accept" class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
<button
class="button-unstyled"
:title="$t('tool_tip.reject_follow_request')" :title="$t('tool_tip.reject_follow_request')"
@click="denyUser()" @click="denyUser()"
> />
<FAIcon
icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
</div> </div>
</div> </div>
<div <div
@ -203,9 +181,8 @@
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>
<StatusContent <status-content
class="faint" class="faint"
:compact="true"
:status="notification.action" :status="notification.action"
/> />
</template> </template>

View file

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

View file

@ -37,6 +37,11 @@
.notification { .notification {
box-sizing: border-box; box-sizing: border-box;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
word-wrap: break-word;
word-break: break-word;
&:hover .animated.Avatar { &:hover .animated.Avatar {
canvas { canvas {
@ -64,6 +69,8 @@
} }
.follow-request-accept { .follow-request-accept {
cursor: pointer;
&:hover { &:hover {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
@ -71,6 +78,8 @@
} }
.follow-request-reject { .follow-request-reject {
cursor: pointer;
&:hover { &:hover {
color: $fallback--cRed; color: $fallback--cRed;
color: var(--cRed, $fallback--cRed); color: var(--cRed, $fallback--cRed);
@ -139,6 +148,13 @@
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
} }
.timeago { .timeago {

View file

@ -11,21 +11,21 @@
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
:model-value="present" :checked="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)" @change="$emit('input', !present ? fallback : undefined)"
/> />
<input <input
:id="name" :id="name"
class="input-number" class="input-number"
type="number" type="number"
:value="modelValue || fallback" :value="value || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
max="1" max="1"
min="0" min="0"
step=".05" step=".05"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('input', $event.target.value)"
> >
</div> </div>
</template> </template>
@ -37,12 +37,11 @@ export default {
Checkbox Checkbox
}, },
props: [ props: [
'name', 'modelValue', 'fallback', 'disabled' 'name', 'value', 'fallback', 'disabled'
], ],
emits: ['update:modelValue'],
computed: { computed: {
present () { present () {
return typeof this.modelValue !== 'undefined' return typeof this.value !== 'undefined'
} }
} }
} }

View file

@ -1,13 +0,0 @@
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
export default {
methods: {
setTransform ({ scale, x, y }) {
this.$el.setTransform({ scale, x, y })
}
},
created () {
// Make lint happy
(() => PinchZoom)()
}
}

View file

@ -1,11 +0,0 @@
<template>
<pinch-zoom
class="pinch-zoom-parent"
v-bind="$attrs"
v-on="$listeners"
>
<slot />
</pinch-zoom>
</template>
<script src="./pinch_zoom.js"></script>

View file

@ -1,14 +1,10 @@
import Timeago from 'components/timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash' import { forEach, map } from 'lodash'
export default { export default {
name: 'Poll', name: 'Poll',
props: ['basePoll', 'emoji'], props: ['basePoll'],
components: { components: { Timeago },
Timeago,
RichContent
},
data () { data () {
return { return {
loading: false, loading: false,
@ -21,7 +17,7 @@ export default {
} }
this.$store.dispatch('trackPoll', this.pollId) this.$store.dispatch('trackPoll', this.pollId)
}, },
unmounted () { destroyed () {
this.$store.dispatch('untrackPoll', this.pollId) this.$store.dispatch('untrackPoll', this.pollId)
}, },
computed: { computed: {

View file

@ -17,11 +17,8 @@
<span class="result-percentage"> <span class="result-percentage">
{{ percentageForOption(option.votes_count) }}% {{ percentageForOption(option.votes_count) }}%
</span> </span>
<RichContent <!-- eslint-disable-next-line vue/no-v-html -->
:html="option.title_html" <span v-html="option.title_html" />
:handle-links="false"
:emoji="emoji"
/>
</div> </div>
<div <div
class="result-fill" class="result-fill"
@ -45,11 +42,8 @@
:value="index" :value="index"
> >
<label class="option-vote"> <label class="option-vote">
<RichContent <!-- eslint-disable-next-line vue/no-v-html -->
:html="option.title_html" <div v-html="option.title_html" />
:handle-links="false"
:emoji="emoji"
/>
</label> </label>
</div> </div>
</div> </div>
@ -71,18 +65,13 @@
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp; {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template> </template>
</div> </div>
<span> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<i18n-t <Timeago
scope="global" :time="expiresAt"
:keypath="expired ? 'polls.expired' : 'polls.expires_in'" :auto-update="60"
> :now-threshold="0"
<Timeago />
:time="expiresAt" </i18n>
:auto-update="60"
:now-threshold="0"
/>
</i18n-t>
</span>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,21 +1,19 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js' import * as DateUtils from 'src/services/date_utils/date_utils.js'
import { uniq } from 'lodash' import { uniq } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import Select from '../select/select.vue'
import { import {
faTimes, faTimes,
faChevronDown,
faPlus faPlus
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faTimes, faTimes,
faChevronDown,
faPlus faPlus
) )
export default { export default {
components: {
Select
},
name: 'PollForm', name: 'PollForm',
props: ['visible'], props: ['visible'],
data: () => ({ data: () => ({

View file

@ -46,19 +46,23 @@
class="poll-type" class="poll-type"
:title="$t('polls.type')" :title="$t('polls.type')"
> >
<Select <label
v-model="pollType" for="poll-type-selector"
class="poll-type-select" class="select"
unstyled="true"
@change="updatePollToParent"
> >
<option value="single"> <select
{{ $t('polls.single_choice') }} v-model="pollType"
</option> class="select"
<option value="multiple"> @change="updatePollToParent"
{{ $t('polls.multiple_choices') }} >
</option> <option value="single">{{ $t('polls.single_choice') }}</option>
</Select> <option value="multiple">{{ $t('polls.multiple_choices') }}</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div> </div>
<div <div
class="poll-expiry" class="poll-expiry"
@ -72,21 +76,24 @@
:max="maxExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit"
@change="expiryAmountChange" @change="expiryAmountChange"
> >
{{ ' ' }} <label class="expiry-unit select">
<Select <select
v-model="expiryUnit" v-model="expiryUnit"
unstyled="true" @change="expiryAmountChange"
class="expiry-unit"
@change="expiryAmountChange"
>
<option
v-for="unit in expiryUnits"
:key="unit"
:value="unit"
> >
{{ $t(`time.${unit}_short`, ['']) }} <option
</option> v-for="unit in expiryUnits"
</Select> :key="unit"
:value="unit"
>
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div> </div>
</div> </div>
</div> </div>
@ -140,8 +147,10 @@
.poll-type { .poll-type {
margin-right: 0.75em; margin-right: 0.75em;
flex: 1 1 60%; flex: 1 1 60%;
.select {
.poll-type-select { border: none;
box-shadow: none;
background-color: transparent;
padding-right: 0.75em; padding-right: 0.75em;
} }
} }
@ -153,6 +162,12 @@
width: 3em; width: 3em;
text-align: right; text-align: right;
} }
.expiry-unit {
border: none;
box-shadow: none;
background-color: transparent;
}
} }
} }
</style> </style>

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