diff --git a/.babelrc b/.babelrc index 373d2c59..79d2b7a4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], "comments": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index cef6d401..d7cc6994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,67 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## 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 a quick settings to timeline header for easier access - Added option to mark posts as sensitive by default - 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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d7c217ce..f666a4ef 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,6 +3,7 @@ Contributors of this project. - Constance Variable (lambadalambda@social.heldscal.la): Code - Coco Snuss (cocosnuss@social.heldscal.la): Code - wakarimasen (wakarimasen@shitposter.club): NSFW hiding image +- eris (eris@disqordia.space): Code - dtluna (dtluna@social.heldscal.la): Code - sonyam (sonyam@social.heldscal.la): Background images - hakui (hakui@freezepeach.xyz): CSS and styling diff --git a/build/dev-server.js b/build/dev-server.js index 48574214..c06192bd 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -21,6 +21,7 @@ var compiler = webpack(webpackConfig) var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, + writeToDisk: true, stats: { colors: true, chunks: false diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index f7ed8b52..4a02852c 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -4,6 +4,7 @@ var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') var { VueLoaderPlugin } = require('vue-loader') +var CopyPlugin = require('copy-webpack-plugin'); var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -95,6 +96,19 @@ module.exports = { entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' }), - new VueLoaderPlugin() + 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, + }, + }) ] } diff --git a/package.json b/package.json index 2370d699..adcfa3d9 100644 --- a/package.json +++ b/package.json @@ -16,106 +16,108 @@ "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "^7.7.6", - "@chenfengyuan/vue-qrcode": "^2.0.0-beta", - "@fortawesome/fontawesome-svg-core": "^1.2.32", - "@fortawesome/free-regular-svg-icons": "^5.15.1", - "@fortawesome/free-solid-svg-icons": "^5.15.1", - "@fortawesome/vue-fontawesome": "^3.0.0-3", - "@vue/compiler-sfc": "^3.0.7", - "body-scroll-lock": "^2.6.4", - "chromatism": "^3.0.0", - "cropperjs": "^1.4.3", - "diff": "^3.0.1", - "escape-html": "^1.0.3", - "localforage": "^1.5.0", - "parse-link-header": "^1.0.1", - "phoenix": "^1.3.0", - "punycode.js": "^2.1.0", - "qrcode": "^1.4.4", - "v-click-outside": "^2.1.1", - "vue": "^3.0.7", - "vue-i18n": "^9.0.0-beta.18", - "vue-router": "^4.0.5", - "vuelidate": "^0.7.6", - "vuex": "^4.0.0" + "@babel/runtime": "7.7.6", + "@chenfengyuan/vue-qrcode": "1.0.2", + "@fortawesome/fontawesome-svg-core": "1.3.0", + "@fortawesome/free-regular-svg-icons": "5.15.4", + "@fortawesome/free-solid-svg-icons": "5.15.4", + "@fortawesome/vue-fontawesome": "2.0.6", + "@kazvmoe-infra/pinch-zoom-element": "1.2.0", + "body-scroll-lock": "2.6.4", + "chromatism": "3.0.0", + "cropperjs": "1.4.3", + "diff": "3.5.0", + "escape-html": "1.0.3", + "localforage": "1.7.3", + "parse-link-header": "1.0.1", + "phoenix": "1.4.0", + "portal-vue": "2.1.7", + "punycode.js": "2.1.0", + "ruffle-mirror": "2021.4.11", + "v-click-outside": "2.1.5", + "vue": "2.6.11", + "vue-i18n": "7.8.1", + "vue-router": "3.0.2", + "vue-template-compiler": "2.6.11", + "vuelidate": "0.7.7", + "vuex": "3.0.1" }, "devDependencies": { - "@babel/core": "^7.7.5", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/preset-env": "^7.7.6", - "@babel/register": "^7.7.4", - "@ungap/event-target": "^0.1.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", - "@vue/babel-plugin-jsx": "^1.0.3", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", - "@vue/test-utils": "^2.0.0-beta.8", - "autoprefixer": "^6.4.0", - "babel-eslint": "^7.0.0", - "babel-loader": "^8.0.6", - "babel-plugin-lodash": "^3.3.4", - "chai": "^3.5.0", - "chalk": "^1.1.3", - "chromedriver": "^87.0.1", - "connect-history-api-fallback": "^1.1.0", - "cross-spawn": "^4.0.2", - "css-loader": "^0.28.0", - "custom-event-polyfill": "^1.0.7", - "eslint": "^5.16.0", - "eslint-config-standard": "^12.0.0", - "eslint-friendly-formatter": "^2.0.5", - "eslint-loader": "^2.1.0", - "eslint-plugin-import": "^2.13.0", - "eslint-plugin-node": "^7.0.0", - "eslint-plugin-promise": "^4.0.0", - "eslint-plugin-standard": "^4.0.0", - "eslint-plugin-vue": "^5.2.2", - "eventsource-polyfill": "^0.9.6", - "express": "^4.13.3", - "file-loader": "^3.0.1", - "function-bind": "^1.0.2", - "html-webpack-plugin": "^3.0.0", - "http-proxy-middleware": "^0.17.2", - "inject-loader": "^2.0.1", - "iso-639-1": "^2.0.3", - "isparta-loader": "^2.0.0", - "json-loader": "^0.5.4", - "karma": "^3.0.0", - "karma-coverage": "^1.1.1", - "karma-firefox-launcher": "^1.1.0", - "karma-mocha": "^1.2.0", - "karma-mocha-reporter": "^2.2.1", - "karma-sinon-chai": "^2.0.2", - "karma-sourcemap-loader": "^0.3.7", - "karma-spec-reporter": "0.0.26", - "karma-webpack": "^4.0.0-rc.3", - "lodash": "^4.16.4", - "lolex": "^1.4.0", - "mini-css-extract-plugin": "^0.5.0", - "mocha": "^3.1.0", - "nightwatch": "^0.9.8", - "opn": "^4.0.2", - "ora": "^0.3.0", - "postcss-loader": "^3.0.0", - "raw-loader": "^0.5.1", - "sass": "^1.17.3", - "sass-loader": "git://github.com/webpack-contrib/sass-loader", + "@babel/core": "7.7.5", + "@babel/plugin-transform-runtime": "7.7.6", + "@babel/preset-env": "7.7.6", + "@babel/register": "7.7.4", + "@ungap/event-target": "0.2.3", + "@vue/babel-helper-vue-jsx-merge-props": "1.2.1", + "@vue/babel-preset-jsx": "1.2.4", + "@vue/test-utils": "1.0.0-beta.28", + "autoprefixer": "6.7.7", + "babel-eslint": "7.2.3", + "babel-loader": "8.0.6", + "babel-plugin-lodash": "3.3.4", + "chai": "3.5.0", + "chalk": "1.1.3", + "chromedriver": "87.0.7", + "connect-history-api-fallback": "1.6.0", + "copy-webpack-plugin": "6.4.1", + "cross-spawn": "4.0.2", + "css-loader": "0.28.11", + "custom-event-polyfill": "1.0.7", + "eslint": "5.16.0", + "eslint-config-standard": "12.0.0", + "eslint-friendly-formatter": "2.0.7", + "eslint-loader": "2.1.2", + "eslint-plugin-import": "2.17.2", + "eslint-plugin-node": "7.0.1", + "eslint-plugin-promise": "4.1.1", + "eslint-plugin-standard": "4.0.0", + "eslint-plugin-vue": "5.2.3", + "eventsource-polyfill": "0.9.6", + "express": "4.16.4", + "file-loader": "3.0.1", + "function-bind": "1.1.1", + "html-webpack-plugin": "3.2.0", + "http-proxy-middleware": "0.17.4", + "inject-loader": "2.0.1", + "iso-639-1": "2.0.3", + "isparta-loader": "2.0.0", + "json-loader": "0.5.7", + "karma": "3.1.4", + "karma-coverage": "1.1.2", + "karma-firefox-launcher": "1.1.0", + "karma-mocha": "1.3.0", + "karma-mocha-reporter": "2.2.5", + "karma-sinon-chai": "2.0.2", + "karma-sourcemap-loader": "0.3.8", + "karma-spec-reporter": "0.0.33", + "karma-webpack": "4.0.2", + "lodash": "4.17.21", + "lolex": "1.6.0", + "mini-css-extract-plugin": "0.5.0", + "mocha": "3.5.3", + "nightwatch": "0.9.21", + "opn": "4.0.2", + "ora": "0.3.0", + "postcss-loader": "3.0.0", + "raw-loader": "0.5.1", + "sass": "1.20.1", + "sass-loader": "7.2.0", "selenium-server": "2.53.1", - "semver": "^5.3.0", - "serviceworker-webpack-plugin": "^1.0.0", - "shelljs": "^0.8.4", - "sinon": "^2.1.0", - "sinon-chai": "^2.8.0", - "stylelint": "^13.6.1", - "stylelint-config-standard": "^20.0.0", - "stylelint-rscss": "^0.4.0", - "url-loader": "^1.1.2", - "vue-loader": "^16.1.2", - "vue-style-loader": "^4.0.0", - "webpack": "^4.0.0", - "webpack-dev-middleware": "^3.6.0", - "webpack-hot-middleware": "^2.12.2", - "webpack-merge": "^0.14.1" + "semver": "5.6.0", + "serviceworker-webpack-plugin": "1.0.1", + "shelljs": "0.8.5", + "sinon": "2.4.1", + "sinon-chai": "2.14.0", + "stylelint": "13.6.1", + "stylelint-config-standard": "20.0.0", + "stylelint-rscss": "0.4.0", + "url-loader": "1.1.2", + "vue-loader": "14.2.4", + "vue-style-loader": "4.1.2", + "webpack": "4.46.0", + "webpack-dev-middleware": "3.7.3", + "webpack-hot-middleware": "2.24.3", + "webpack-merge": "0.14.1" }, "engines": { "node": ">= 4.0.0", diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/src/App.js b/src/App.js index f886d7cb..c4360af5 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import Notifications from './components/notifications/notifications.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' -import ChatPanel from './components/chat_panel/chat_panel.vue' +import ShoutPanel from './components/shout_panel/shout_panel.vue' import SettingsModal from './components/settings_modal/settings_modal.vue' import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' @@ -26,7 +26,7 @@ export default { InstanceSpecificPanel, FeaturesPanel, WhoToFollowPanel, - ChatPanel, + ShoutPanel, MediaModal, SideDrawer, MobilePostStatusButton, @@ -65,7 +65,7 @@ export default { } } }, - chat () { return this.$store.state.chat.channel.state === 'joined' }, + shout () { return this.$store.state.shout.channel.state === 'joined' }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && @@ -73,11 +73,17 @@ export default { this.$store.state.instance.instanceSpecificPanelContent }, 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 }, privateMode () { return this.$store.state.instance.private }, sidebarAlign () { return { - 'order': this.$store.state.instance.sidebarRight ? 99 : 0 + 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 } }, ...mapGetters(['mergedConfig']) diff --git a/src/App.scss b/src/App.scss index 7cb92fc9..bc027f4f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -88,6 +88,10 @@ a { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); + &.-sublime { + background: transparent; + } + i[class*=icon-], .svg-inline--fa { color: $fallback--text; @@ -187,7 +191,7 @@ a { } } -input, textarea, .select, .input { +input, textarea, .input { &.unstyled { border-radius: 0; @@ -217,47 +221,11 @@ input, textarea, .select, .input { hyphens: none; padding: 8px .5em; - &.select { - padding: 0; - } - - &:disabled, &[disabled=disabled] { + &:disabled, &[disabled=disabled], &.disabled { cursor: not-allowed; 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] { background: none; border: none; @@ -830,13 +798,6 @@ nav { } } -.select-multiple { - display: flex; - .option-list { - margin: 0; - padding-left: .5em; - } -} .setting-list, .option-list{ list-style-type: none; diff --git a/src/App.vue b/src/App.vue index 702b69d1..31e8fd40 100644 --- a/src/App.vue +++ b/src/App.vue @@ -49,10 +49,11 @@ - diff --git a/src/_variables.scss b/src/_variables.scss index 9004d551..099d3606 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px; $fallback--chatMessageRadius: 10px; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + +$status-margin: 0.75em; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 6f84f686..e0d7aa0f 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -121,6 +121,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('nsfwCensorImage') copyInstanceOption('background') copyInstanceOption('hidePostStats') + copyInstanceOption('hideBotIndication') copyInstanceOption('hideUserStats') copyInstanceOption('hideFilteredStatuses') copyInstanceOption('logo') @@ -246,7 +247,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) 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: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) diff --git a/src/boot/routes.js b/src/boot/routes.js index b5d3c631..1bc1f9f7 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -16,7 +16,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import Notifications from 'components/notifications/notifications.vue' import AuthForm from 'components/auth_form/auth_form.js' -import ChatPanel from 'components/chat_panel/chat_panel.vue' +import ShoutPanel from 'components/shout_panel/shout_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import About from 'components/about/about.vue' import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' @@ -64,7 +64,7 @@ export default (store) => { { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 5f5779a0..d62a4adc 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -1,4 +1,5 @@ import StillImage from '../still-image/still-image.vue' +import Flash from '../flash/flash.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' @@ -10,7 +11,12 @@ import { faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight } from '@fortawesome/free-solid-svg-icons' library.add( @@ -19,36 +25,64 @@ library.add( faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight ) const Attachment = { props: [ 'attachment', + 'description', + 'hideDescription', 'nsfw', 'size', - 'allowPlay', 'setMedia', - 'naturalSizeLoad' + 'remove', + 'shiftUp', + 'shiftDn', + 'edit' ], data () { return { + localDescription: this.description || this.attachment.description, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, preloadImage: this.$store.getters.mergedConfig.preloadImage, loading: false, img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), modalOpen: false, - showHidden: false + showHidden: false, + flashLoaded: false, + showDescription: false } }, components: { + Flash, StillImage, VideoAttachment }, computed: { + classNames () { + return [ + { + '-loading': this.loading, + '-nsfw-placeholder': this.hidden, + '-editable': this.edit !== undefined + }, + '-type-' + this.type, + this.size && '-size-' + this.size, + `-${this.useContainFit ? 'contain' : 'cover'}-fit` + ] + }, usePlaceholder () { - return this.size === 'hide' || this.type === 'unknown' + return this.size === 'hide' + }, + useContainFit () { + return this.$store.getters.mergedConfig.useContainFit }, placeholderName () { if (this.attachment.description === '' || !this.attachment.description) { @@ -72,24 +106,33 @@ const Attachment = { return this.nsfw && this.hideNsfwLocal && !this.showHidden }, isEmpty () { - 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' + return (this.type === 'html' && !this.attachment.oembed) }, useModal () { - const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] - : this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] + let modalTypes = [] + switch (this.size) { + case 'hide': + case 'small': + modalTypes = ['image', 'video', 'audio', 'flash'] + break + default: + modalTypes = this.mergedConfig.playVideosInModal + ? ['image', 'video', 'flash'] + : ['image'] + break + } return modalTypes.includes(this.type) }, + videoTag () { + return this.useModal ? 'button' : 'span' + }, ...mapGetters(['mergedConfig']) }, + watch: { + localDescription (newVal) { + this.onEdit(newVal) + } + }, methods: { linkClicked ({ target }) { if (target.tagName === 'A') { @@ -98,12 +141,37 @@ const Attachment = { }, openModal (event) { if (this.useModal) { - event.stopPropagation() - event.preventDefault() - this.setMedia() - this.$store.dispatch('setCurrent', this.attachment) + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + } else if (this.type === 'unknown') { + window.open(this.attachment.url) } }, + openModalForce (event) { + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + }, + onEdit (event) { + this.edit && this.edit(this.attachment, event) + }, + onRemove () { + this.remove && this.remove(this.attachment) + }, + onShiftUp () { + this.shiftUp && this.shiftUp(this.attachment) + }, + onShiftDn () { + this.shiftDn && this.shiftDn(this.attachment) + }, + stopFlash () { + this.$refs.flash.closePlayer() + }, + setFlashLoaded (event) { + this.flashLoaded = event + }, + toggleDescription () { + this.showDescription = !this.showDescription + }, toggleHidden (event) { if ( (this.mergedConfig.useOneClickNsfw && !this.showHidden) && @@ -130,7 +198,7 @@ const Attachment = { onImageLoad (image) { const width = image.naturalWidth const height = image.naturalHeight - this.naturalSizeLoad && this.naturalSizeLoad({ width, height }) + this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) } } } diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss new file mode 100644 index 00000000..dfda15bf --- /dev/null +++ b/src/components/attachment/attachment.scss @@ -0,0 +1,268 @@ +@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; + } + } +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 2c1c1682..2a89886d 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,7 +1,8 @@ - + diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index 87085a28..8f41e2fb 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const BasicUserCard = { @@ -13,7 +14,8 @@ const BasicUserCard = { }, components: { UserCard, - UserAvatar + UserAvatar, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index c53f6a9c..53deb1df 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -25,17 +25,11 @@ :title="user.name" class="basic-user-card-user-name" > - - - - {{ user.name }}
${this.$t('chats.you')} ${content}` : content return { summary: '', - statusnet_html: messagePreview, + emojis: messageEmojis, + raw_html: messagePreview, text: messagePreview, attachments: [] } diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 9e97b28e..57332bed 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -77,18 +77,15 @@ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } - .StatusContent { - img.emoji { - width: 1.4em; - height: 1.4em; - } + .chat-preview-body { + --emoji-size: 1.4em; } .time-wrapper { line-height: 1.4em; } - .single-line { + .chat-preview-body { padding-right: 1em; } } diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index cd3f436e..c7c0e878 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -29,7 +29,8 @@
- diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index bb380f87..eb195bc1 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -57,8 +57,9 @@ const ChatMessage = { messageForStatusContent () { return { summary: '', - statusnet_html: this.message.content, - text: this.message.content, + emojis: this.message.emojis, + raw_html: this.message.content || '', + text: this.message.content || '', attachments: this.message.attachments } }, diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index e4351d3b..1913479f 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -1,6 +1,7 @@ @import '../../_variables.scss'; .chat-message-wrapper { + &.hovered-message-chain { .animated.Avatar { canvas { @@ -40,6 +41,12 @@ .chat-message { display: flex; padding-bottom: 0.5em; + + .status-body:hover { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; + } } .avatar-wrapper { @@ -62,10 +69,6 @@ &.with-media { width: 100%; - .gallery-row { - overflow: hidden; - } - .status { width: 100%; } @@ -89,8 +92,9 @@ } .without-attachment { - .status-content { - &::after { + .message-content { + // TODO figure out how to do it properly + .RichContent::after { margin-right: 5.4em; content: " "; display: inline-block; @@ -162,6 +166,7 @@ .visible { opacity: 1; } + } .chat-message-date-separator { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 0f3fc97d..d62b831d 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -71,6 +71,7 @@
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index b16ed39d..a92028e8 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -1,5 +1,4 @@ @@ -34,6 +34,8 @@ white-space: nowrap; align-items: center; + --emoji-size: 14px; + .username { max-width: 100%; text-overflow: ellipsis; @@ -41,14 +43,6 @@ display: inline; word-wrap: break-word; overflow: hidden; - text-overflow: ellipsis; - - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .Avatar { diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 069c0b40..2ef2977a 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,5 +1,19 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' 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 idA = a.type === 'retweet' ? a.retweeted_status.id : a.id @@ -35,7 +49,10 @@ const conversation = { data () { return { highlight: null, - expanded: false + expanded: false, + threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' + statusContentPropertiesObject: {}, + inlineDivePosition: null } }, props: [ @@ -53,12 +70,50 @@ const conversation = { } }, computed: { - hideStatus () { - if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { - return this.virtualHidden && this.$refs.statusComponent[0].suspendable - } else { - return this.virtualHidden + 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 () { + return this.virtualHidden && this.suspendable }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] @@ -90,6 +145,121 @@ const conversation = { 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 () { let i = 1 // eslint-disable-next-line camelcase @@ -109,15 +279,71 @@ const conversation = { }, {}) }, isExpanded () { - return this.expanded || this.isPage + return !!(this.expanded || this.isPage) }, hiddenStyle () { const height = (this.status && this.status.virtualHeight) || '120px' 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: { - Status + Status, + ThreadTree }, watch: { statusId (newVal, oldVal) { @@ -132,6 +358,8 @@ const conversation = { expanded (value) { if (value) { this.fetchConversation() + } else { + this.resetDisplayState() } }, virtualHidden (value) { @@ -161,8 +389,8 @@ const conversation = { getReplies (id) { return this.replies[id] || [] }, - focused (id) { - return (this.isExpanded) && id === this.statusId + getHighlight () { + return this.isExpanded ? this.highlight : null }, setHighlight (id) { if (!id) return @@ -170,15 +398,139 @@ const conversation = { this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, - getHighlight () { - return this.isExpanded ? this.highlight : null - }, toggleExpanded () { this.expanded = !this.expanded }, getConversationId (statusId) { const status = this.$store.state.statuses.allStatusesObject[statusId] 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 = {} } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 3fb26d92..7628ceaa 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -18,24 +18,168 @@ {{ $t('timeline.collapse') }} - +
+
+
+ + + + {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} + + +
+
+
+ +
+
+ + + + {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} + + +
+
+
+
+ +
+
+ +
+
.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); } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 762aa610..304baf9d 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -52,6 +52,7 @@ href="/pleroma/admin/#/login-pleroma" class="nav-icon" target="_blank" + @click.stop >
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index d3e4680f..137ef9c0 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,13 +1,10 @@ -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import { set } from 'vue' +import Select from '../select/select.vue' export default { + components: { + Select + }, props: [ 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' ], @@ -39,8 +36,8 @@ export default { return this.dValue.family }, set (v) { - this.lValue.family = v - this.$emit('update:modelValue', this.lValue) + set(this.lValue, 'family', v) + this.$emit('input', this.lValue) } }, isCustom () { diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index 0cee2edc..ddda218b 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -22,30 +22,20 @@ class="opt-l" :for="name + '-o'" /> - + {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} + + 1) { - // if 1 attachment on last row -> add it to the previous row instead - const lastAttachment = last(rows)[0] - const allButLastRow = dropRight(rows) - last(allButLastRow).push(lastAttachment) - return allButLastRow + const attachments = this.limit > 0 + ? this.attachments.slice(0, this.limit) + : this.attachments + if (this.size === 'hide') { + return attachments.map(item => ({ minimal: true, items: [item] })) } + 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 }, - useContainFit () { - return this.$store.getters.mergedConfig.useContainFit + attachmentsDimensionalScore () { + return this.rows.reduce((acc, row) => { + 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: { - onNaturalSizeLoad (id, size) { - this.sizes[id] = size + onNaturalSizeLoad ({ id, width, height }) { + this.$set(this.sizes, id, { width, height }) }, - rowStyle (itemsPerRow) { - return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` } + rowStyle (row) { + if (row.audio) { + return { 'padding-bottom': '25%' } // fixed reduced height for audio + } else if (!row.minimal && !row.grid) { + return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } + } }, itemStyle (id, row) { const total = sumBy(row, item => this.getAspectRatio(item.id)) @@ -46,6 +101,16 @@ const Gallery = { getAspectRatio (id) { const size = this.sizes[id] 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) } } } diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index ca91c9c1..f2e1b5ce 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -1,26 +1,84 @@