Compare commits

..

20 Commits

Author SHA1 Message Date
Shpuld Shpuldson 6aa276374f update changelog for 2.2.0 2020-11-06 19:35:01 +02:00
Shpuld Shpludson 23232e1c8f Merge branch 'develop' into 'master'
Merge develop into master for 2.2.0

See merge request pleroma/pleroma-fe!1278
2020-11-06 17:27:25 +00:00
Shpuld Shpludson b225c3578f Merge branch 'rc/2.1.2' into 'master'
Merge 2.1.2 to MASTER

See merge request pleroma/pleroma-fe!1236
2020-09-17 17:26:22 +00:00
Shpuld Shpludson 30efbaab34 Merge branch 'master' into 'rc/2.1.2'
# Conflicts:
#   CHANGELOG.md
2020-09-17 17:17:21 +00:00
Shpuld Shpuldson 8ca0586c0f update changelog for 2.1.2 2020-09-17 20:15:08 +03:00
Shpuld Shpludson 938887ef91 Merge branch 'rc/2.1.1' into 'master'
Update master with 2.1.1

See merge request pleroma/pleroma-fe!1231
2020-09-08 09:46:19 +00:00
Shpuld Shpludson f31bc5310e Merge branch 'master' into 'rc/2.1.1'
# Conflicts:
#   CHANGELOG.md
2020-09-08 09:35:36 +00:00
Shpuld Shpuldson 925bf5b5a4 update changelog with 2.1.1 2020-09-08 12:30:45 +03:00
Shpuld Shpludson e768ec1fca Merge branch '2.1.0-rc0' into 'master'
2.1.0 into master

See merge request pleroma/pleroma-fe!1217
2020-08-27 14:45:03 +00:00
Shpuld Shpuldson d09f43ba7a Merge branch '2.1.0-rc0' of git.pleroma.social:pleroma/pleroma-fe into 2.1.0-rc0 2020-08-27 17:13:43 +03:00
Shpuld Shpuldson 748c4d8c71 fix boomarks mistake in changelog 2020-08-27 17:13:14 +03:00
Shpuld Shpludson c7ddfefe34 Merge branch 'master' into '2.1.0-rc0'
# Conflicts:
#   CHANGELOG.md
#   src/services/api/api.service.js
2020-08-27 13:59:56 +00:00
Shpuld Shpuldson c0205d582a add missing release to changelog to fix conflicts 2020-08-27 16:56:47 +03:00
Shpuld Shpuldson 4ac882a3b0 add back unreleased/patch 2020-08-27 16:51:16 +03:00
Shpuld Shpuldson bdbc4b27b6 set 2.1.0 date and remove rc from changelog 2020-08-27 16:49:01 +03:00
Shpuld Shpuldson 42598fc675 change changelog 2020-08-26 14:28:43 +03:00
Shpuld Shpludson 5d49edc823 Merge branch 'rc/2.0.5' into 'master'
Update MASTER for 2.0.5 patch

See merge request pleroma/pleroma-fe!1105
2020-05-12 17:36:05 +00:00
Shpuld Shpuldson 0bc0a8d5f5 update changelog for 2.0.5 2020-05-12 20:27:37 +03:00
Shpuld Shpuldson 726d5279c1 Revert "remove with_move param"
This reverts commit 02c8a9e314.
2020-05-12 20:04:00 +03:00
Shpuld Shpludson a0f780c455 Merge branch 'rc/2.0.3' into 'master'
Update MASTER with 2.0.3 for real

See merge request pleroma/pleroma-fe!1099
2020-05-02 14:17:27 +00:00
208 changed files with 4407 additions and 13767 deletions
-1
View File
@@ -1 +0,0 @@
rinpatch <rin@patch.cx> <rinpatch@sdf.org>
+1 -87
View File
@@ -3,90 +3,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [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
### Fixed
- Button to remove uploaded media in post status form is now properly placed and sized.
- Fixed shoutbox not working in mobile layout
- Fixed missing highlighted border in expanded conversations again
- Fixed some UI jumpiness when opening images particularly in chat view
- Fixed chat unread badge looking weird
- Fixed punycode names not working properly
- Fixed notifications crashing on an invalid notification
### Changed
- Display 'people voted' instead of 'votes' for multi-choice polls
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
- Renamed "Timeline" to "Home Timeline" to be more clear
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
- When opening emoji picker or react picker, it automatically focuses the search field
- Language picker now uses native language names
### Added
- Added reason field for registration when approval is required
- Group staff members by role in the About page
## [2.2.3] - 2021-01-18
### Added
- Added Report button to status ellipsis menu for easier reporting
### Fixed
- Follows/Followers tabs on user profiles now display the content properly.
- Handle punycode in screen names
- Fixed local dev mode having non-functional websockets in some cases
- Show notices for websocket events (errors, abnormal closures, reconnections)
- Fix not being able to re-enable websocket until page refresh
- Fix annoying issue where timeline might have few posts when streaming is enabled
### Changed
- Don't filter own posts when they hit your wordfilter
## [2.2.2] - 2020-12-22
### Added
- Mouseover titles for emojis in reaction picker
- Support to input emoji into the search box in reaction picker
- Added some missing unicode emoji
- Added the upload limit to the Features panel in the About page
- Support for solid color wallpaper, instance doesn't have to define a wallpaper anymore
### Fixed
- Fixed the occasional bug where screen would scroll 1px when typing into a reply form
- Fixed timeline errors locking timelines
- Fixed missing highlighted border in expanded conversations
- Fixed custom emoji not working in profile field names
- Fixed pinned statuses not appearing in user profiles
- Fixed some elements not being keyboard navigation friendly
- Fixed error handling when updating various profile images
- Fixed your latest chat messages disappearing when closing chat view and opening it again during the same session
- Fixed custom emoji not showing in poll options before voting
- Fixed link color not applied to instance name in topbar
### Changed
- Errors when fetching are now shown with popup errors instead of "Error fetching updates" in panel headers
- Made reply/fav/repeat etc buttons easier to hit
- Adjusted timeline menu clickable area to match the visible button
- Moved external source link from status heading to the ellipsis menu
- Disabled horizontal textarea resize
- Wallpaper is now top-aligned, horizontally centered.
## [2.2.1] - 2020-11-11
### Fixed
- Fixed regression in react popup alignment and overflowing
## [2.2.0] - 2020-11-06 ## [2.2.0] - 2020-11-06
### Added ### Added
@@ -95,8 +11,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Import/export a muted users - Import/export a muted users
- Proper handling of deletes when using websocket streaming - Proper handling of deletes when using websocket streaming
- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent - Added optimistic chat message sending, so you can start writing next message before the previous one has been sent
- Added a small red badge to the favicon when there's unread notifications
- Added the NSFW alert to link previews
### Fixed ### Fixed
- Fixed clicking NSFW hider through status popover - Fixed clicking NSFW hider through status popover
@@ -118,7 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.1.2] - 2020-09-17 ## [2.1.2] - 2020-09-17
### Fixed ### Fixed
- Fixed chats list not updating its order when new messages come in - Fixed chats list not updating its order when new messages come in
- Fixed chat messages sometimes getting lost when you receive a message at the same time - Fixed chat messages sometimes getting lost when you receive a message at the same time
## [2.1.1] - 2020-09-08 ## [2.1.1] - 2020-09-08
-1
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
-1
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
-14
View File
@@ -3,7 +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 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
@@ -94,19 +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'
}),
// 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,
},
}) })
] ]
} }
-5
View File
@@ -3,11 +3,6 @@ const path = require('path')
let settings = {} let settings = {}
try { try {
settings = require('./local.json') settings = require('./local.json')
if (settings.target && settings.target.endsWith('/')) {
// replacing trailing slash since it can conflict with some apis
// and that's how actual BE reports its url
settings.target = settings.target.replace(/\/$/, '')
}
console.log('Using local dev server settings (/config/local.json):') console.log('Using local dev server settings (/config/local.json):')
console.log(JSON.stringify(settings, null, 2)) console.log(JSON.stringify(settings, null, 2))
} catch (e) { } catch (e) {
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<title>Pleroma</title>
<!--server-generated-meta--> <!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.png">
</head> </head>
+4 -6
View File
@@ -31,10 +31,9 @@
"parse-link-header": "^1.0.1", "parse-link-header": "^1.0.1",
"phoenix": "^1.3.0", "phoenix": "^1.3.0",
"portal-vue": "^2.1.4", "portal-vue": "^2.1.4",
"punycode.js": "^2.1.0",
"ruffle-mirror": "^2021.4.10",
"v-click-outside": "^2.1.1", "v-click-outside": "^2.1.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
@@ -56,9 +55,8 @@
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"chai": "^3.5.0", "chai": "^3.5.0",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"chromedriver": "^87.0.1", "chromedriver": "^2.21.2",
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"copy-webpack-plugin": "^6.4.1",
"cross-spawn": "^4.0.2", "cross-spawn": "^4.0.2",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
@@ -104,7 +102,7 @@
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "^5.3.0", "semver": "^5.3.0",
"serviceworker-webpack-plugin": "^1.0.0", "serviceworker-webpack-plugin": "^1.0.0",
"shelljs": "^0.8.4", "shelljs": "^0.7.4",
"sinon": "^2.1.0", "sinon": "^2.1.0",
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"stylelint": "^13.6.1", "stylelint": "^13.6.1",
@@ -113,7 +111,7 @@
"url-loader": "^1.1.2", "url-loader": "^1.1.2",
"vue-loader": "^14.0.0", "vue-loader": "^14.0.0",
"vue-style-loader": "^4.0.0", "vue-style-loader": "^4.0.0",
"webpack": "^4.44.0", "webpack": "^4.0.0",
"webpack-dev-middleware": "^3.6.0", "webpack-dev-middleware": "^3.6.0",
"webpack-hot-middleware": "^2.12.2", "webpack-hot-middleware": "^2.12.2",
"webpack-merge": "^0.14.1" "webpack-merge": "^0.14.1"
+14 -20
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'
@@ -15,7 +15,6 @@ import UserReportingModal from './components/user_reporting_modal/user_reporting
import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex'
export default { export default {
name: 'app', name: 'app',
@@ -26,7 +25,7 @@ export default {
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
ShoutPanel, ChatPanel,
MediaModal, MediaModal,
SideDrawer, SideDrawer,
MobilePostStatusButton, MobilePostStatusButton,
@@ -51,21 +50,20 @@ export default {
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image }, background () {
instanceBackground () { return this.currentUser.background_image || this.$store.state.instance.background
return this.mergedConfig.hideInstanceWallpaper
? null
: this.$store.state.instance.background
}, },
background () { return this.userBackground || this.instanceBackground },
bgStyle () { bgStyle () {
if (this.background) { return {
return { 'background-image': `url(${this.background})`
'--body-background-image': `url(${this.background})`
}
} }
}, },
shout () { return this.$store.state.shout.channel.state === 'joined' }, bgAppStyle () {
return {
'--body-background-image': `url(${this.background})`
}
},
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 +71,13 @@ 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 },
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'])
}, },
methods: { methods: {
updateMobileState () { updateMobileState () {
+72 -94
View File
@@ -14,9 +14,7 @@
right: -20px; right: -20px;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: var(--wallpaper); background-position: 0 50%;
background-image: var(--body-background-image);
background-position: 50% 50px;
} }
i[class^='icon-'] { i[class^='icon-'] {
@@ -35,7 +33,6 @@ h4 {
max-width: 980px; max-width: 980px;
align-content: flex-start; align-content: flex-start;
} }
.underlay { .underlay {
background-color: rgba(0,0,0,0.15); background-color: rgba(0,0,0,0.15);
background-color: var(--underlay, rgba(0,0,0,0.15)); background-color: var(--underlay, rgba(0,0,0,0.15));
@@ -72,7 +69,7 @@ a {
color: var(--link, $fallback--link); color: var(--link, $fallback--link);
} }
.button-default { button {
user-select: none; user-select: none;
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
@@ -88,8 +85,7 @@ a {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
i[class*=icon-], i[class*=icon-], .svg-inline--fa {
.svg-inline--fa {
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
} }
@@ -111,8 +107,7 @@ a {
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg); background-color: var(--btnPressed, $fallback--fg);
svg, svg, i {
i {
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
} }
@@ -125,8 +120,7 @@ a {
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg); background-color: var(--btnDisabled, $fallback--fg);
svg, svg, i {
i {
color: $fallback--text; color: $fallback--text;
color: var(--btnDisabledText, $fallback--text); color: var(--btnDisabledText, $fallback--text);
} }
@@ -140,8 +134,7 @@ a {
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
svg, svg, i {
i {
color: $fallback--text; color: $fallback--text;
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
} }
@@ -156,38 +149,7 @@ a {
} }
} }
.button-unstyled { input, textarea, .select, .input {
background: none;
border: none;
outline: none;
display: inline;
text-align: initial;
font-size: 100%;
font-family: inherit;
padding: 0;
line-height: unset;
cursor: pointer;
box-sizing: content-box;
color: inherit;
&.-link {
color: $fallback--link;
color: var(--link, $fallback--link);
}
&.-fullwidth {
width: 100%;
}
&.-hover-highlight {
&:hover svg {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
}
input, textarea, .input {
&.unstyled { &.unstyled {
border-radius: 0; border-radius: 0;
@@ -217,11 +179,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;
@@ -305,10 +303,6 @@ input, textarea, .input {
box-sizing: border-box; box-sizing: border-box;
} }
} }
&.resize-height {
resize: vertical;
}
} }
option { option {
@@ -448,7 +442,6 @@ main-router {
color: $fallback--faint; color: $fallback--faint;
color: var(--panelFaint, $fallback--faint); color: var(--panelFaint, $fallback--faint);
} }
.faint-link { .faint-link {
color: $fallback--faint; color: $fallback--faint;
color: var(--faintLink, $fallback--faint); color: var(--faintLink, $fallback--faint);
@@ -460,8 +453,11 @@ main-router {
overflow-x: hidden; overflow-x: hidden;
} }
.button-default, button {
.alert { flex-shrink: 0;
}
button, .alert {
// height: 100%; // height: 100%;
line-height: 21px; line-height: 21px;
min-height: 0; min-height: 0;
@@ -472,11 +468,8 @@ main-router {
align-self: stretch; align-self: stretch;
} }
.button-default { button {
flex-shrink: 0; &, i[class*=icon-] {
&,
i[class*=icon-] {
color: $fallback--text; color: $fallback--text;
color: var(--btnPanelText, $fallback--text); color: var(--btnPanelText, $fallback--text);
} }
@@ -499,8 +492,7 @@ main-router {
} }
} }
a, a {
.-link {
color: $fallback--link; color: $fallback--link;
color: var(--panelLink, $fallback--link) color: var(--panelLink, $fallback--link)
} }
@@ -511,31 +503,19 @@ main-router {
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
} }
/* TODO Should remove timeline-footer from here when we refactor panels into .panel-footer {
* separate component and utilize slots
*/
.panel-footer, .timeline-footer {
display: flex;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
flex: none;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
align-items: baseline;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
.faint { .faint {
color: $fallback--faint; color: $fallback--faint;
color: var(--panelFaint, $fallback--faint); color: var(--panelFaint, $fallback--faint);
} }
a, a {
.-link {
color: $fallback--link; color: $fallback--link;
color: var(--panelLink, $fallback--link); color: var(--panelLink, $fallback--link)
} }
} }
@@ -562,7 +542,6 @@ nav {
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow); box-shadow: var(--topBarShadow);
box-sizing: border-box;
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
@@ -682,15 +661,6 @@ nav {
color: var(--alertWarningPanelText, $fallback--text); color: var(--alertWarningPanelText, $fallback--text);
} }
} }
&.success {
background-color: var(--alertSuccess, $fallback--alertWarning);
color: var(--alertSuccessText, $fallback--text);
.panel-heading & {
color: var(--alertSuccessPanelText, $fallback--text);
}
}
} }
.faint { .faint {
@@ -794,6 +764,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;
@@ -820,7 +797,7 @@ nav {
} }
} }
.btn.button-default { .btn.btn-default {
min-height: 28px; min-height: 28px;
} }
@@ -840,10 +817,16 @@ nav {
} }
.new-status-notification { .new-status-notification {
position: relative; position:relative;
margin-top: -1px;
font-size: 1.1em; font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
padding: 10px;
z-index: 1; z-index: 1;
flex: 1; background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
} }
.chat-layout { .chat-layout {
@@ -851,11 +834,6 @@ nav {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
// Get rid of scrollbar on body as scrolling happens on different element
body {
overflow: hidden;
}
// Ensures the fixed position of the mobile browser bars on scroll up / down events. // Ensures the fixed position of the mobile browser bars on scroll up / down events.
// Prevents the mobile browser bars from overlapping or hiding the message posting form. // Prevents the mobile browser bars from overlapping or hiding the message posting form.
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
+5 -4
View File
@@ -1,11 +1,12 @@
<template> <template>
<div <div
id="app" id="app"
:style="bgStyle" :style="bgAppStyle"
> >
<div <div
id="app_bg_wrapper" id="app_bg_wrapper"
class="app-bg-wrapper" class="app-bg-wrapper"
:style="bgStyle"
/> />
<MobileNav v-if="isMobileLayout" /> <MobileNav v-if="isMobileLayout" />
<DesktopNav v-else /> <DesktopNav v-else />
@@ -49,10 +50,10 @@
</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"
/> />
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
+1 -5
View File
@@ -7,7 +7,6 @@ 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'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js' import { applyTheme } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
let staticInitialResults = null let staticInitialResults = null
@@ -51,7 +50,6 @@ const getInstanceConfig = async ({ store }) => {
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@@ -240,7 +238,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') })
@@ -328,8 +326,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
const width = windowWidth() const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800) store.dispatch('setMobileLayout', width <= 800)
FaviconService.initFaviconService()
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
+2 -2
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'
@@ -64,7 +64,7 @@ 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 },
@@ -35,7 +35,7 @@ const AccountActions = {
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
}, },
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', this.user.id)
}, },
openChat () { openChat () {
this.$router.push({ this.$router.push({
@@ -4,21 +4,23 @@
trigger="click" trigger="click"
placement="bottom" placement="bottom"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
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
v-if="relationship.showing_reblogs" v-if="relationship.showing_reblogs"
class="btn button-default dropdown-item" class="btn btn-default dropdown-item"
@click="hideRepeats" @click="hideRepeats"
> >
{{ $t('user_card.hide_repeats') }} {{ $t('user_card.hide_repeats') }}
</button> </button>
<button <button
v-if="!relationship.showing_reblogs" v-if="!relationship.showing_reblogs"
class="btn button-default dropdown-item" class="btn btn-default dropdown-item"
@click="showRepeats" @click="showRepeats"
> >
{{ $t('user_card.show_repeats') }} {{ $t('user_card.show_repeats') }}
@@ -30,41 +32,42 @@
</template> </template>
<button <button
v-if="relationship.blocking" v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item" class="btn btn-default btn-block dropdown-item"
@click="unblockUser" @click="unblockUser"
> >
{{ $t('user_card.unblock') }} {{ $t('user_card.unblock') }}
</button> </button>
<button <button
v-else v-else
class="btn button-default btn-block dropdown-item" class="btn btn-default btn-block dropdown-item"
@click="blockUser" @click="blockUser"
> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</button> </button>
<button <button
class="btn button-default btn-block dropdown-item" class="btn btn-default btn-block dropdown-item"
@click="reportUser" @click="reportUser"
> >
{{ $t('user_card.report') }} {{ $t('user_card.report') }}
</button> </button>
<button <button
v-if="pleromaChatMessagesAvailable" v-if="pleromaChatMessagesAvailable"
class="btn button-default btn-block dropdown-item" class="btn btn-default btn-block dropdown-item"
@click="openChat" @click="openChat"
> >
{{ $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="btn btn-default ellipsis-button"
class="icon" >
icon="ellipsis-v" <FAIcon
/> class="icon"
</button> icon="ellipsis-v"
</template> />
</div>
</Popover> </Popover>
</div> </div>
</template> </template>
@@ -79,6 +82,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;
@@ -8,7 +8,7 @@
{{ $t('general.error_retry') }} {{ $t('general.error_retry') }}
</p> </p>
<button <button
class="btn button-default" class="btn"
@click="retry" @click="retry"
> >
{{ $t('general.retry') }} {{ $t('general.retry') }}
+2 -8
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'
@@ -9,18 +8,14 @@ import {
faFile, faFile,
faMusic, faMusic,
faImage, faImage,
faVideo, faVideo
faPlayCircle,
faTimes
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faFile, faFile,
faMusic, faMusic,
faImage, faImage,
faVideo, faVideo
faPlayCircle,
faTimes
) )
const Attachment = { const Attachment = {
@@ -44,7 +39,6 @@ const Attachment = {
} }
}, },
components: { components: {
Flash,
StillImage, StillImage,
VideoAttachment VideoAttachment
}, },
+12 -24
View File
@@ -42,13 +42,15 @@
icon="play-circle" icon="play-circle"
/> />
</a> </a>
<button <div
v-if="nsfw && hideNsfwLocal && !hidden" v-if="nsfw && hideNsfwLocal && !hidden"
class="button-unstyled hider" class="hider"
@click.prevent="toggleHidden"
> >
<FAIcon icon="times" /> <a
</button> href="#"
@click.prevent="toggleHidden"
>Hide</a>
</div>
<a <a
v-if="type === 'image' && (!hidden || preloadImage)" v-if="type === 'image' && (!hidden || preloadImage)"
@@ -117,11 +119,6 @@
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
</div> </div>
<Flash
v-if="type === 'flash'"
:src="attachment.large_thumb_url || attachment.url"
/>
</div> </div>
</template> </template>
@@ -177,7 +174,6 @@
} }
.non-gallery.attachment { .non-gallery.attachment {
&.flash,
&.video { &.video {
flex: 1 0 40%; flex: 1 0 40%;
} }
@@ -238,23 +234,15 @@
.hider { .hider {
position: absolute; position: absolute;
right: 0; right: 0;
white-space: nowrap;
margin: 10px; margin: 10px;
padding: 0; padding: 5px;
background: rgba(230,230,230,0.6);
font-weight: bold;
z-index: 4; z-index: 4;
line-height: 1;
border-radius: $fallback--tooltipRadius; border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $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 { video {
@@ -42,7 +42,7 @@
class="basic-user-card-screen-name" class="basic-user-card-screen-name"
:to="userProfileLink(user)" :to="userProfileLink(user)"
> >
@{{ user.screen_name_ui }} @{{ user.screen_name }}
</router-link> </router-link>
</div> </div>
<slot /> <slot />
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="block-card-content-container"> <div class="block-card-content-container">
<button <button
v-if="blocked" v-if="blocked"
class="btn button-default" class="btn btn-default"
:disabled="progress" :disabled="progress"
@click="unblockUser" @click="unblockUser"
> >
@@ -16,7 +16,7 @@
</button> </button>
<button <button
v-else v-else
class="btn button-default" class="btn btn-default"
:disabled="progress" :disabled="progress"
@click="blockUser" @click="blockUser"
> >
+1 -9
View File
@@ -73,7 +73,7 @@ const Chat = {
}, },
formPlaceholder () { formPlaceholder () {
if (this.recipient) { if (this.recipient) {
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui }) return this.$t('chats.message_user', { nickname: this.recipient.screen_name })
} else { } else {
return '' return ''
} }
@@ -234,13 +234,6 @@ const Chat = {
const scrollable = this.$refs.scrollable const scrollable = this.$refs.scrollable
return scrollable && scrollable.scrollTop <= 0 return scrollable && scrollable.scrollTop <= 0
}, },
cullOlderCheck () {
window.setTimeout(() => {
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
}
}, 5000)
},
handleScroll: _.throttle(function () { handleScroll: _.throttle(function () {
if (!this.currentChat) { return } if (!this.currentChat) { return }
@@ -248,7 +241,6 @@ const Chat = {
this.fetchChat({ maxId: this.currentChatMessageService.minId }) this.fetchChat({ maxId: this.currentChatMessageService.minId })
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.jumpToBottomButtonVisible = false this.jumpToBottomButtonVisible = false
this.cullOlderCheck()
if (this.newMessageCount > 0) { if (this.newMessageCount > 0) {
// Use a delay before marking as read to prevent situation where new messages // Use a delay before marking as read to prevent situation where new messages
// arrive just as you're leaving the view and messages that you didn't actually // arrive just as you're leaving the view and messages that you didn't actually
+3 -3
View File
@@ -98,10 +98,10 @@
.unread-message-count { .unread-message-count {
font-size: 0.8em; font-size: 0.8em;
left: 50%; left: 50%;
transform: translate(-50%, 0);
border-radius: 100%;
margin-top: -1rem; margin-top: -1rem;
padding: 0.1em; padding: 0;
border-radius: 50px;
position: absolute;
} }
.chat-loading-error { .chat-loading-error {
+5 -5
View File
@@ -10,10 +10,7 @@
<span class="title"> <span class="title">
{{ $t("chats.chats") }} {{ $t("chats.chats") }}
</span> </span>
<button <button @click="newChat">
class="button-default"
@click="newChat"
>
{{ $t("chats.new") }} {{ $t("chats.new") }}
</button> </button>
</div> </div>
@@ -23,7 +20,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"
@@ -31,6 +31,9 @@
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
border-radius: $fallback--chatMessageRadius;
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
} }
.popover { .popover {
+16 -18
View File
@@ -50,37 +50,35 @@
@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="dropdown-item dropdown-item-icon"
@click="deleteMessage" @click="deleteMessage"
> >
<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="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
: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>
@@ -5,8 +5,6 @@
</template> </template>
<script> <script>
import localeService from 'src/services/locale/locale.service.js'
export default { export default {
name: 'Timeago', name: 'Timeago',
props: ['date'], props: ['date'],
@@ -18,7 +16,7 @@ export default {
if (this.date.getTime() === today.getTime()) { if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today') return this.$t('display_date.today')
} else { } else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
} }
} }
} }
@@ -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 () {
@@ -35,19 +35,7 @@ const shoutPanel = {
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames) return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
} }
},
watch: {
messages (newVal) {
const scrollEl = this.$el.querySelector('.chat-window')
if (!scrollEl) return
if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) {
this.$nextTick(() => {
if (!scrollEl) return
scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight
})
}
}
} }
} }
export default shoutPanel export default chatPanel
@@ -1,50 +1,52 @@
<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">
{{ $t('shoutbox.title') }} <span>{{ $t('shoutbox.title') }}</span>
<FAIcon <FAIcon
v-if="floating" v-if="floating"
icon="times" icon="times"
class="close-icon"
/> />
</div> </div>
</div> </div>
<div class="shout-window"> <div
v-chat-scroll
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 +55,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,12 +74,12 @@
</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; right: 0px;
bottom: 0px; bottom: 0px;
@@ -85,39 +87,32 @@
max-width: 25em; max-width: 25em;
} }
.shout-panel { .chat-panel {
.shout-heading { .chat-heading {
cursor: pointer; cursor: pointer;
.icon { .icon {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
margin-right: 0.5em;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
} }
} }
.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;
@@ -128,7 +123,7 @@
} }
} }
.shout-input { .chat-input {
display: flex; display: flex;
textarea { textarea {
flex: 1; flex: 1;
@@ -138,7 +133,7 @@
} }
} }
.shout-panel { .chat-panel {
.title { .title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
+1 -1
View File
@@ -12,7 +12,7 @@ export default Vue.component('chat-title', {
], ],
computed: { computed: {
title () { title () {
return this.user ? this.user.screen_name_ui : '' return this.user ? this.user.screen_name : ''
}, },
htmlTitle () { htmlTitle () {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''
+14 -7
View File
@@ -10,13 +10,12 @@
class="panel-heading conversation-heading" class="panel-heading conversation-heading"
> >
<span class="title"> {{ $t('timeline.conversation') }} </span> <span class="title"> {{ $t('timeline.conversation') }} </span>
<button <span v-if="collapsable">
v-if="collapsable" <a
class="button-unstyled -link" href="#"
@click.prevent="toggleExpanded" @click.prevent="toggleExpanded"
> >{{ $t('timeline.collapse') }}</a>
{{ $t('timeline.collapse') }} </span>
</button>
</div> </div>
<status <status
v-for="status in conversation" v-for="status in conversation"
@@ -50,6 +49,7 @@
.Conversation { .Conversation {
.conversation-status { .conversation-status {
border-left: none;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);
@@ -57,6 +57,13 @@
} }
&.-expanded { &.-expanded {
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
}
.conversation-status:last-child { .conversation-status:last-child {
border-bottom: none; border-bottom: none;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+5 -10
View File
@@ -5,10 +5,6 @@
width: 100%; width: 100%;
position: fixed; position: fixed;
a {
color: var(--topBarLink, $fallback--link);
}
.inner-nav { .inner-nav {
display: grid; display: grid;
grid-template-rows: 50px; grid-template-rows: 50px;
@@ -25,7 +21,7 @@
grid-template-areas: "logo sitename actions"; grid-template-areas: "logo sitename actions";
} }
.button-default { button {
&, svg { &, svg {
color: $fallback--text; color: $fallback--text;
color: var(--btnTopBarText, $fallback--text); color: var(--btnTopBarText, $fallback--text);
@@ -84,13 +80,12 @@
.nav-icon { .nav-icon {
margin-left: 0.2em; margin-left: 0.2em;
width: 2em; width: 2em;
height: 100%;
text-align: center; text-align: center;
}
.svg-inline--fa { a, a svg {
color: $fallback--link; color: $fallback--link;
color: var(--topBarLink, $fallback--link); color: var(--topBarLink, $fallback--link);
}
} }
.sitename { .sitename {
+19 -21
View File
@@ -36,8 +36,9 @@
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop.native @click.stop.native
/> />
<button <a
class="button-unstyled nav-icon" href="#"
class="nav-icon"
@click.stop="openSettingsModal" @click.stop="openSettingsModal"
> >
<FAIcon <FAIcon
@@ -46,32 +47,29 @@
icon="cog" icon="cog"
:title="$t('nav.preferences')" :title="$t('nav.preferences')"
/> />
</button> </a>
<a <a
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma" href="/pleroma/admin/#/login-pleroma"
class="nav-icon" class="nav-icon"
target="_blank" target="_blank"
> ><FAIcon
<FAIcon fixed-width
fixed-width class="fa-scale-110 fa-old-padding"
class="fa-scale-110 fa-old-padding" icon="tachometer-alt"
icon="tachometer-alt" :title="$t('nav.administration')"
:title="$t('nav.administration')" /></a>
/> <a
</a>
<button
v-if="currentUser" v-if="currentUser"
class="button-unstyled nav-icon" href="#"
class="nav-icon"
@click.prevent="logout" @click.prevent="logout"
> ><FAIcon
<FAIcon fixed-width
fixed-width class="fa-scale-110 fa-old-padding"
class="fa-scale-110 fa-old-padding" icon="sign-out-alt"
icon="sign-out-alt" :title="$t('login.logout')"
:title="$t('login.logout')" /></a>
/>
</button>
</div> </div>
</div> </div>
</nav> </nav>
@@ -6,20 +6,20 @@
<ProgressButton <ProgressButton
v-if="muted" v-if="muted"
:click="unmuteDomain" :click="unmuteDomain"
class="btn button-default" class="btn btn-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>
<ProgressButton <ProgressButton
v-else v-else
:click="muteDomain" :click="muteDomain"
class="btn button-default" class="btn btn-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>
+46 -57
View File
@@ -57,7 +57,6 @@ const EmojiInput = {
required: true, required: true,
type: Function type: Function
}, },
// TODO VUE3: change to modelValue, change 'input' event to 'input'
value: { value: {
/** /**
* Used for v-model * Used for v-model
@@ -115,8 +114,7 @@ const EmojiInput = {
showPicker: false, showPicker: false,
temporarilyHideSuggestions: false, temporarilyHideSuggestions: false,
keepOpen: false, keepOpen: false,
disableClickOutside: false, disableClickOutside: false
suggestions: []
} }
}, },
components: { components: {
@@ -126,6 +124,21 @@ const EmojiInput = {
padEmoji () { padEmoji () {
return this.$store.getters.mergedConfig.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
suggestions () {
const firstchar = this.textAtCaret.charAt(0)
if (this.textAtCaret === firstchar) { return [] }
const matchedSuggestions = this.suggest(this.textAtCaret)
if (matchedSuggestions.length <= 0) {
return []
}
return take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }, index) => ({
...rest,
// eslint-disable-next-line camelcase
img: imageUrl || '',
highlighted: index === this.highlighted
}))
},
showSuggestions () { showSuggestions () {
return this.focused && return this.focused &&
this.suggestions && this.suggestions &&
@@ -144,68 +157,45 @@ const EmojiInput = {
} }
}, },
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: {
showSuggestions: function (newValue) { showSuggestions: function (newValue) {
this.$emit('shown', newValue) this.$emit('shown', newValue)
},
textAtCaret: async function (newWord) {
const firstchar = newWord.charAt(0)
this.suggestions = []
if (newWord === firstchar) return
const matchedSuggestions = await this.suggest(newWord)
// Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return
this.suggestions = take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }) => ({
...rest,
img: imageUrl || ''
}))
},
suggestions (newValue) {
this.$nextTick(this.resize)
} }
}, },
methods: { methods: {
focusPickerInput () {
const pickerEl = this.$refs.picker.$el
if (!pickerEl) return
const pickerInput = pickerEl.querySelector('input')
if (pickerInput) pickerInput.focus()
},
triggerShowPicker () { triggerShowPicker () {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad() this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
this.focusPickerInput()
}) })
// This temporarily disables "click outside" handler // This temporarily disables "click outside" handler
// since external trigger also means click originates // since external trigger also means click originates
@@ -216,12 +206,11 @@ 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()
this.$refs.picker.startEmojiLoad() this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput)
} }
}, },
replace (replacement) { replace (replacement) {
@@ -262,13 +251,13 @@ const EmojiInput = {
this.$emit('input', 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
}) })
}, },
@@ -285,9 +274,9 @@ const EmojiInput = {
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()
@@ -349,7 +338,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) {
@@ -414,8 +403,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
@@ -444,7 +433,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()
} }
} }
@@ -480,7 +469,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)
@@ -494,7 +483,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) {
+4 -6
View File
@@ -1,20 +1,18 @@
<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 }"
> >
<slot /> <slot />
<template v-if="enableEmojiPicker"> <template v-if="enableEmojiPicker">
<button <div
v-if="!hideEmojiButton" v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon" class="emoji-picker-icon"
type="button"
@click.prevent="togglePicker" @click.prevent="togglePicker"
> >
<FAIcon :icon="['far', 'smile-beam']" /> <FAIcon :icon="['far', 'smile-beam']" />
</button> </div>
<EmojiPicker <EmojiPicker
v-if="enableEmojiPicker" v-if="enableEmojiPicker"
ref="picker" ref="picker"
@@ -39,7 +37,7 @@
v-for="(suggestion, index) in suggestions" v-for="(suggestion, index) in suggestions"
:key="index" :key="index"
class="autocomplete-item" class="autocomplete-item"
:class="{ highlighted: index === highlighted }" :class="{ highlighted: suggestion.highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span class="image"> <span class="image">
+57 -81
View File
@@ -1,3 +1,4 @@
import { debounce } from 'lodash'
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions: * data: object providing source information for specific types of suggestions:
@@ -10,19 +11,19 @@
* doesn't support user linking you can just provide only emoji. * doesn't support user linking you can just provide only emoji.
*/ */
export default data => { const debounceUserSearch = debounce((data, input) => {
const emojiCurry = suggestEmoji(data.emoji) data.updateUsersList(input)
const usersCurry = data.store && suggestUsers(data.store) }, 500)
return input => {
const firstChar = input[0] export default data => input => {
if (firstChar === ':' && data.emoji) { const firstChar = input[0]
return emojiCurry(input) if (firstChar === ':' && data.emoji) {
} return suggestEmoji(data.emoji)(input)
if (firstChar === '@' && usersCurry) {
return usersCurry(input)
}
return []
} }
if (firstChar === '@' && data.users) {
return suggestUsers(data)(input)
}
return []
} }
export const suggestEmoji = emojis => input => { export const suggestEmoji = emojis => input => {
@@ -56,75 +57,50 @@ export const suggestEmoji = emojis => input => {
}) })
} }
export const suggestUsers = ({ dispatch, state }) => { export const suggestUsers = data => input => {
// Keep some persistent values in closure, most importantly for the const noPrefix = input.toLowerCase().substr(1)
// custom debounce to work. Lodash debounce does not return a promise. const users = data.users
let suggestions = []
let previousQuery = ''
let timeout = null
let cancelUserSearch = null
const userSearch = (query) => dispatch('searchUsers', { query }) const newUsers = users.filter(
const debounceUserSearch = (query) => { user =>
cancelUserSearch && cancelUserSearch() user.screen_name.toLowerCase().startsWith(noPrefix) ||
return new Promise((resolve, reject) => { user.name.toLowerCase().startsWith(noPrefix)
timeout = setTimeout(() => {
userSearch(query).then(resolve).catch(reject) /* taking only 20 results so that sorting is a bit cheaper, we display
}, 300) * only 5 anyway. could be inaccurate, but we ideally we should query
cancelUserSearch = () => { * backend anyway
clearTimeout(timeout) */
resolve([]) ).slice(0, 20).sort((a, b) => {
} let aScore = 0
}) let bScore = 0
}
// Matches on screen name (i.e. user@instance) makes a priority
return async input => { aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
const noPrefix = input.toLowerCase().substr(1) bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
if (previousQuery === noPrefix) return suggestions
// Matches on name takes second priority
suggestions = [] aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
previousQuery = noPrefix bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
// Fetch more and wait, don't fetch if there's the 2nd @ because
// the backend user search can't deal with it. const diff = (bScore - aScore) * 10
// Reference semantics make it so that we get the updated data after
// the await. // Then sort alphabetically
if (!noPrefix.includes('@')) { const nameAlphabetically = a.name > b.name ? 1 : -1
await debounceUserSearch(noPrefix) const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
}
return diff + nameAlphabetically + screenNameAlphabetically
const newSuggestions = state.users.users.filter( /* eslint-disable camelcase */
user => }).map(({ screen_name, name, profile_image_url_original }) => ({
user.screen_name.toLowerCase().startsWith(noPrefix) || displayText: screen_name,
user.name.toLowerCase().startsWith(noPrefix) detailText: name,
).slice(0, 20).sort((a, b) => { imageUrl: profile_image_url_original,
let aScore = 0 replacement: '@' + screen_name + ' '
let bScore = 0 }))
// Matches on screen name (i.e. user@instance) makes a priority // BE search users to get more comprehensive results
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 if (data.updateUsersList) {
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 debounceUserSearch(data, noPrefix)
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
const diff = (bScore - aScore) * 10
// Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
displayText: screen_name_ui,
detailText: name,
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
/* eslint-enable camelcase */
suggestions = newSuggestions || []
return suggestions
} }
return newUsers
/* eslint-enable camelcase */
} }
@@ -6,7 +6,7 @@
:users="accountsForEmoji[reaction.name]" :users="accountsForEmoji[reaction.name]"
> >
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn btn-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
@@ -0,0 +1,102 @@
<template>
<div class="import-export-container">
<slot name="before" />
<button
class="btn"
@click="exportData"
>
{{ exportLabel }}
</button>
<button
class="btn"
@click="importData"
>
{{ importLabel }}
</button>
<slot name="afterButtons" />
<p
v-if="importFailed"
class="alert error"
>
{{ importFailedText }}
</p>
<slot name="afterError" />
</div>
</template>
<script>
export default {
props: [
'exportObject',
'importLabel',
'exportLabel',
'importFailedText',
'validator',
'onImport',
'onImportFailure'
],
data () {
return {
importFailed: false
}
},
methods: {
exportData () {
const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
e.setAttribute('download', 'pleroma_theme.json')
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.style.display = 'none'
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
},
importData () {
this.importFailed = false
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
filePicker.setAttribute('accept', '.json')
filePicker.addEventListener('change', event => {
if (event.target.files[0]) {
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
const valid = this.validator(parsed)
if (valid) {
this.onImport(parsed)
} else {
this.importFailed = true
// this.onImportFailure(valid)
}
} catch (e) {
// This will happen both if there is a JSON syntax error or the theme is missing components
this.importFailed = true
// this.onImportFailure(e)
}
}
reader.readAsText(event.target.files[0])
}
})
document.body.appendChild(filePicker)
filePicker.click()
document.body.removeChild(filePicker)
}
}
}
</script>
<style lang="scss">
.import-export-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
</style>
+1 -1
View File
@@ -11,7 +11,7 @@
</div> </div>
<button <button
v-else v-else
class="btn button-default" class="btn btn-default"
@click="process" @click="process"
> >
{{ exportButtonLabel }} {{ exportButtonLabel }}
+3 -10
View File
@@ -5,12 +5,10 @@ import {
faBookmark, faBookmark,
faEyeSlash, faEyeSlash,
faThumbtack, faThumbtack,
faShareAlt, faShareAlt
faExternalLinkAlt
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
faBookmark as faBookmarkReg, faBookmark as faBookmarkReg
faFlag
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
library.add( library.add(
@@ -19,9 +17,7 @@ library.add(
faBookmarkReg, faBookmarkReg,
faEyeSlash, faEyeSlash,
faThumbtack, faThumbtack,
faShareAlt, faShareAlt
faExternalLinkAlt,
faFlag
) )
const ExtraButtons = { const ExtraButtons = {
@@ -68,9 +64,6 @@ const ExtraButtons = {
this.$store.dispatch('unbookmark', { id: this.status.id }) this.$store.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch(err => this.$emit('onError', err.error.error))
},
reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
} }
}, },
computed: { computed: {
+26 -56
View File
@@ -1,17 +1,18 @@
<template> <template>
<Popover <Popover
class="ExtraButtons"
trigger="click" trigger="click"
placement="top" placement="top"
:offset="{ y: 5 }" class="extra-button-popover"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
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"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="muteConversation" @click.prevent="muteConversation"
> >
<FAIcon <FAIcon
@@ -21,7 +22,7 @@
</button> </button>
<button <button
v-if="canMute && status.thread_muted" v-if="canMute && status.thread_muted"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="unmuteConversation" @click.prevent="unmuteConversation"
> >
<FAIcon <FAIcon
@@ -31,7 +32,7 @@
</button> </button>
<button <button
v-if="!status.pinned && canPin" v-if="!status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="pinStatus" @click.prevent="pinStatus"
@click="close" @click="close"
> >
@@ -42,7 +43,7 @@
</button> </button>
<button <button
v-if="status.pinned && canPin" v-if="status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus" @click.prevent="unpinStatus"
@click="close" @click="close"
> >
@@ -53,7 +54,7 @@
</button> </button>
<button <button
v-if="!status.bookmarked" v-if="!status.bookmarked"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="bookmarkStatus" @click.prevent="bookmarkStatus"
@click="close" @click="close"
> >
@@ -64,7 +65,7 @@
</button> </button>
<button <button
v-if="status.bookmarked" v-if="status.bookmarked"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="unbookmarkStatus" @click.prevent="unbookmarkStatus"
@click="close" @click="close"
> >
@@ -75,7 +76,7 @@
</button> </button>
<button <button
v-if="canDelete" v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus" @click.prevent="deleteStatus"
@click="close" @click="close"
> >
@@ -85,7 +86,7 @@
/><span>{{ $t("status.delete") }}</span> /><span>{{ $t("status.delete") }}</span>
</button> </button>
<button <button
class="button-default dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="copyLink" @click.prevent="copyLink"
@click="close" @click="close"
> >
@@ -94,38 +95,14 @@
icon="share-alt" icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span> /><span>{{ $t("status.copy_link") }}</span>
</button> </button>
<a
v-if="!status.is_local"
class="button-default dropdown-item dropdown-item-icon"
title="Source"
:href="status.external_url"
target="_blank"
>
<FAIcon
fixed-width
icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span>
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="reportStatus"
@click="close"
>
<FAIcon
fixed-width
:icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span>
</button>
</div> </div>
</template> </div>
<template v-slot:trigger> <span slot="trigger">
<button class="button-unstyled popover-trigger"> <FAIcon
<FAIcon class="ExtraButtons fa-scale-110 fa-old-padding"
class="fa-scale-110 fa-old-padding" icon="ellipsis-h"
icon="ellipsis-h" />
/> </span>
</button>
</template>
</Popover> </Popover>
</template> </template>
@@ -135,20 +112,13 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.ExtraButtons { .ExtraButtons {
/* override of popover internal stuff */ cursor: pointer;
.popover-trigger-button { position: static;
width: auto;
}
.popover-trigger { &:hover,
position: static; .extra-button-popover.open & {
padding: 10px; color: $fallback--text;
margin: -10px; color: var(--text, $fallback--text);
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
} }
} }
</style> </style>
@@ -31,6 +31,11 @@ const FavoriteButton = {
} }
}, },
computed: { computed: {
classes () {
return {
'-favorited': this.status.favorited
}
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
} }
} }
@@ -1,31 +1,23 @@
<template> <template>
<div class="FavoriteButton"> <div v-if="loggedIn">
<button <FAIcon
v-if="loggedIn" :class="classes"
class="button-unstyled interactive" class="FavoriteButton fa-scale-110 fa-old-padding -interactive"
:class="status.favorited && '-favorited'"
:title="$t('tool_tip.favorite')" :title="$t('tool_tip.favorite')"
:icon="[status.favorited ? 'fas' : 'far', 'star']"
:spin="animated"
@click.prevent="favorite()" @click.prevent="favorite()"
> />
<FAIcon <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
class="fa-scale-110 fa-old-padding" </div>
:icon="[status.favorited ? 'fas' : 'far', 'star']" <div v-else>
:spin="animated" <FAIcon
/> :class="classes"
</button> class="FavoriteButton fa-scale-110 fa-old-padding"
<span v-else> :title="$t('tool_tip.favorite')"
<FAIcon :icon="['far', 'star']"
class="fa-scale-110 fa-old-padding" />
:title="$t('tool_tip.favorite')" <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
:icon="['far', 'star']"
/>
</span>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
class="action-counter"
>
{{ status.fave_num }}
</span>
</div> </div>
</template> </template>
@@ -35,28 +27,19 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.FavoriteButton { .FavoriteButton {
display: flex; &.-interactive {
cursor: pointer;
animation-duration: 0.6s;
> :first-child { &:hover {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
.svg-inline--fa {
animation-duration: 0.6s;
}
&:hover .svg-inline--fa,
&.-favorited .svg-inline--fa {
color: $fallback--cOrange; color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange); color: var(--cOrange, $fallback--cOrange);
} }
} }
&.-favorited {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
} }
</style> </style>
@@ -1,15 +1,12 @@
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
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 },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode }, minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
textlimit: function () { return this.$store.state.instance.textlimit }, textlimit: function () { return this.$store.state.instance.textlimit }
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
} }
} }
@@ -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') }}
@@ -25,7 +25,6 @@
</li> </li>
<li>{{ $t('features_panel.scope_options') }}</li> <li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li> <li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
</ul> </ul>
</div> </div>
</div> </div>
-52
View File
@@ -1,52 +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
})
},
closePlayer () {
console.log(this.ruffleInstance)
this.ruffleInstance.remove()
this.player = false
}
}
}
export default Flash
-88
View File
@@ -1,88 +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>
<button
v-if="player"
class="button-unstyled hider"
@click="closePlayer"
>
<FAIcon icon="stop" />
</button>
</div>
</template>
<script src="./flash.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.Flash {
width: 100%;
height: 260px;
position: relative;
.player {
height: 100%;
width: 100%;
}
.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';
}
.placeholder {
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
@@ -1,6 +1,6 @@
<template> <template>
<button <button
class="btn button-default follow-button" class="btn btn-default follow-button"
:class="{ toggled: isPressed }" :class="{ toggled: isPressed }"
:disabled="inProgress" :disabled="inProgress"
:title="title" :title="title"
@@ -2,13 +2,13 @@
<basic-user-card :user="user"> <basic-user-card :user="user">
<div class="follow-request-card-content-container"> <div class="follow-request-card-content-container">
<button <button
class="btn button-default" class="btn btn-default"
@click="approveUser" @click="approveUser"
> >
{{ $t('user_card.approve') }} {{ $t('user_card.approve') }}
</button> </button>
<button <button
class="btn button-default" class="btn btn-default"
@click="denyUser" @click="denyUser"
> >
{{ $t('user_card.deny') }} {{ $t('user_card.deny') }}
+8 -4
View File
@@ -1,10 +1,14 @@
import { set } from 'vue' 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', 'value', 'fallback', 'options', 'no-inherit' 'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
], ],
+22 -13
View File
@@ -22,20 +22,30 @@
class="opt-l" class="opt-l"
:for="name + '-o'" :for="name + '-o'"
/> />
<Select <label
:id="name + '-font-switcher'" :for="name + '-font-switcher'"
v-model="preset" class="select"
: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"
@@ -55,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;
} }
@@ -9,15 +9,11 @@
<div class="notice-message"> <div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }} {{ $t(notice.messageKey, notice.messageArgs) }}
</div> </div>
<button <FAIcon
class="button-unstyled close-notice" class="fa-scale-110 fa-old-padding"
icon="times"
@click="closeNotice(notice)" @click="closeNotice(notice)"
> />
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div> </div>
</div> </div>
</template> </template>
@@ -58,7 +54,7 @@
.global-error { .global-error {
background-color: var(--alertPopupError, $fallback--cRed); background-color: var(--alertPopupError, $fallback--cRed);
color: var(--alertPopupErrorText, $fallback--text); color: var(--alertPopupErrorText, $fallback--text);
.svg-inline--fa { i {
color: var(--alertPopupErrorText, $fallback--text); color: var(--alertPopupErrorText, $fallback--text);
} }
} }
@@ -66,32 +62,17 @@
.global-warning { .global-warning {
background-color: var(--alertPopupWarning, $fallback--cOrange); background-color: var(--alertPopupWarning, $fallback--cOrange);
color: var(--alertPopupWarningText, $fallback--text); color: var(--alertPopupWarningText, $fallback--text);
.svg-inline--fa { i {
color: var(--alertPopupWarningText, $fallback--text); color: var(--alertPopupWarningText, $fallback--text);
} }
} }
.global-success {
background-color: var(--alertPopupSuccess, $fallback--cGreen);
color: var(--alertPopupSuccessText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupSuccessText, $fallback--text);
}
}
.global-info { .global-info {
background-color: var(--alertPopupNeutral, $fallback--fg); background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text); color: var(--alertPopupNeutralText, $fallback--text);
.svg-inline--fa { i {
color: var(--alertPopupNeutralText, $fallback--text); color: var(--alertPopupNeutralText, $fallback--text);
} }
} }
.close-notice {
padding-right: 0.2em;
.svg-inline--fa:hover {
opacity: 0.6;
}
}
} }
</style> </style>
+14 -1
View File
@@ -2,10 +2,12 @@ import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css' import 'cropperjs/dist/cropper.css'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes,
faCircleNotch faCircleNotch
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faTimes,
faCircleNotch faCircleNotch
) )
@@ -51,7 +53,8 @@ const ImageCropper = {
cropper: undefined, cropper: undefined,
dataUrl: undefined, dataUrl: undefined,
filename: undefined, filename: undefined,
submitting: false submitting: false,
submitError: null
} }
}, },
computed: { computed: {
@@ -63,6 +66,9 @@ const ImageCropper = {
}, },
cancelText () { cancelText () {
return this.cancelButtonLabel || this.$t('image_cropper.cancel') return this.cancelButtonLabel || this.$t('image_cropper.cancel')
},
submitErrorMsg () {
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
} }
}, },
methods: { methods: {
@@ -76,8 +82,12 @@ const ImageCropper = {
}, },
submit (cropping = true) { submit (cropping = true) {
this.submitting = true this.submitting = true
this.avatarUploadError = null
this.submitHandler(cropping && this.cropper, this.file) this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy()) .then(() => this.destroy())
.catch((err) => {
this.submitError = err
})
.finally(() => { .finally(() => {
this.submitting = false this.submitting = false
}) })
@@ -103,6 +113,9 @@ const ImageCropper = {
reader.readAsDataURL(this.file) reader.readAsDataURL(this.file)
this.$emit('changed', this.file, reader) this.$emit('changed', this.file, reader)
} }
},
clearError () {
this.submitError = null
} }
}, },
mounted () { mounted () {
+14 -3
View File
@@ -11,21 +11,21 @@
</div> </div>
<div class="image-cropper-buttons-wrapper"> <div class="image-cropper-buttons-wrapper">
<button <button
class="button-default btn" class="btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="submit()" @click="submit()"
v-text="saveText" v-text="saveText"
/> />
<button <button
class="button-default btn" class="btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="destroy" @click="destroy"
v-text="cancelText" v-text="cancelText"
/> />
<button <button
class="button-default btn" class="btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="submit(false)" @click="submit(false)"
@@ -37,6 +37,17 @@
icon="circle-notch" icon="circle-notch"
/> />
</div> </div>
<div
v-if="submitError"
class="alert error"
>
{{ submitErrorMsg }}
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError"
/>
</div>
</div> </div>
<input <input
ref="input" ref="input"
+1 -1
View File
@@ -15,7 +15,7 @@
/> />
<button <button
v-else v-else
class="btn button-default" class="btn btn-default"
@click="submit" @click="submit"
> >
{{ submitButtonLabel }} {{ submitButtonLabel }}
@@ -3,35 +3,51 @@
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }} {{ $t('settings.interfaceLanguage') }}
</label> </label>
<Select <label
id="interface-language-switcher" for="interface-language-switcher"
v-model="language" class="select"
> >
<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="(langCode, i) in languageCodes"
</Select> :key="langCode"
:value="langCode"
>
{{ languageNames[i] }}
</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div> </div>
</template> </template>
<script> <script>
import languagesObject from '../../i18n/messages' import languagesObject from '../../i18n/messages'
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 () { languageCodes () {
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) return languagesObject.languages
},
languageNames () {
return _.map(this.languageCodes, this.getLanguageName)
}, },
language: { language: {
@@ -45,13 +61,11 @@ export default {
methods: { methods: {
getLanguageName (code) { getLanguageName (code) {
const specialLanguageNames = { const specialLanguageNames = {
'ja_easy': 'やさしいにほんご', 'ja': 'Japanese (日本語)',
'zh': '简体中文', 'ja_easy': 'Japanese (やさしいにほんご)',
'zh_Hant': '繁體中文' 'zh': 'Chinese (简体中文)'
} }
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code) return specialLanguageNames[code] || ISO6391.getName(code)
const browserLocale = localeService.internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
} }
} }
} }
+2 -13
View File
@@ -1,5 +1,3 @@
import { mapGetters } from 'vuex'
const LinkPreview = { const LinkPreview = {
name: 'LinkPreview', name: 'LinkPreview',
props: [ props: [
@@ -17,20 +15,11 @@ const LinkPreview = {
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid // Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
// as it makes sure to hide the image if somehow NSFW tagged preview can // as it makes sure to hide the image if somehow NSFW tagged preview can
// exist. // exist.
return this.card.image && !this.censored && this.size !== 'hide' return this.card.image && !this.nsfw && this.size !== 'hide'
},
censored () {
return this.nsfw && this.hideNsfwConfig
}, },
useDescription () { useDescription () {
return this.card.description && /\S/.test(this.card.description) return this.card.description && /\S/.test(this.card.description)
}, }
hideNsfwConfig () {
return this.mergedConfig.hideNsfw
},
...mapGetters([
'mergedConfig'
])
}, },
created () { created () {
if (this.useImage) { if (this.useImage) {
+6 -11
View File
@@ -9,17 +9,12 @@
<div <div
v-if="useImage && imageLoaded" v-if="useImage && imageLoaded"
class="card-image" class="card-image"
:class="{ 'small-image': size === 'small' }"
> >
<img :src="card.image"> <img :src="card.image">
</div> </div>
<div class="card-content"> <div class="card-content">
<span class="card-host faint"> <span class="card-host faint">{{ card.provider_name }}</span>
<span
v-if="censored"
class="nsfw-alert alert warning"
>{{ $t('status.nsfw') }}</span>
{{ card.provider_name }}
</span>
<h4 class="card-title">{{ card.title }}</h4> <h4 class="card-title">{{ card.title }}</h4>
<p <p
v-if="useDescription" v-if="useDescription"
@@ -55,6 +50,10 @@
} }
} }
.small-image {
width: 80px;
}
.card-content { .card-content {
max-height: 100%; max-height: 100%;
margin: 0.5em; margin: 0.5em;
@@ -77,10 +76,6 @@
max-height: calc(1.2em * 3 - 1px); max-height: calc(1.2em * 3 - 1px);
} }
.nsfw-alert {
margin: 2em 0;
}
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
border-style: solid; border-style: solid;
+1 -1
View File
@@ -61,7 +61,7 @@
<button <button
:disabled="loggingIn" :disabled="loggingIn"
type="submit" type="submit"
class="btn button-default" class="btn btn-default"
> >
{{ $t('login.login') }} {{ $t('login.login') }}
</button> </button>
@@ -73,21 +73,11 @@
} }
} }
@keyframes media-fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-image { .modal-image {
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
image-orientation: from-image; // NOTE: only FF supports this image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
} }
.modal-view-button-arrow { .modal-view-button-arrow {
+32 -22
View File
@@ -1,29 +1,33 @@
<template> <template>
<label <div
class="media-upload" class="media-upload"
:class="{ disabled: disabled }" :class="{ disabled: disabled }"
:title="$t('tool_tip.media_upload')"
> >
<FAIcon <label
v-if="uploading" class="label"
class="progress-icon" :title="$t('tool_tip.media_upload')"
icon="circle-notch"
spin
/>
<FAIcon
v-if="!uploading"
class="new-icon"
icon="upload"
/>
<input
v-if="uploadReady"
:disabled="disabled"
type="file"
style="position: fixed; top: -100em"
multiple="true"
@change="change"
> >
</label> <FAIcon
v-if="uploading"
class="progress-icon"
icon="circle-notch"
spin
/>
<FAIcon
v-if="!uploading"
class="new-icon"
icon="upload"
/>
<input
v-if="uploadReady"
:disabled="disabled"
type="file"
style="position: fixed; top: -100em"
multiple="true"
@change="change"
>
</label>
</div>
</template> </template>
<script src="./media_upload.js" ></script> <script src="./media_upload.js" ></script>
@@ -32,6 +36,12 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.media-upload { .media-upload {
cursor: pointer; .label {
display: inline-block;
}
.new-icon {
cursor: pointer;
}
} }
</style> </style>
+7 -9
View File
@@ -23,25 +23,23 @@
<div class="form-group"> <div class="form-group">
<div class="login-bottom"> <div class="login-bottom">
<div> <div>
<button <a
class="button-unstyled -link" href="#"
type="button"
@click.prevent="requireTOTP" @click.prevent="requireTOTP"
> >
{{ $t('login.enter_two_factor_code') }} {{ $t('login.enter_two_factor_code') }}
</button> </a>
<br> <br>
<button <a
class="button-unstyled -link" href="#"
type="button"
@click.prevent="abortMFA" @click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </a>
</div> </div>
<button <button
type="submit" type="submit"
class="btn button-default" class="btn btn-default"
> >
{{ $t('general.verify') }} {{ $t('general.verify') }}
</button> </button>
+7 -9
View File
@@ -25,25 +25,23 @@
<div class="form-group"> <div class="form-group">
<div class="login-bottom"> <div class="login-bottom">
<div> <div>
<button <a
class="button-unstyled -link" href="#"
type="button"
@click.prevent="requireRecovery" @click.prevent="requireRecovery"
> >
{{ $t('login.enter_recovery_code') }} {{ $t('login.enter_recovery_code') }}
</button> </a>
<br> <br>
<button <a
class="button-unstyled -link" href="#"
type="button"
@click.prevent="abortMFA" @click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </a>
</div> </div>
<button <button
type="submit" type="submit"
class="btn button-default" class="btn btn-default"
> >
{{ $t('general.verify') }} {{ $t('general.verify') }}
</button> </button>
+8 -6
View File
@@ -9,8 +9,9 @@
@click="scrollToTop()" @click="scrollToTop()"
> >
<div class="item"> <div class="item">
<button <a
class="button-unstyled mobile-nav-button" href="#"
class="mobile-nav-button"
@click.stop.prevent="toggleMobileSidebar()" @click.stop.prevent="toggleMobileSidebar()"
> >
<FAIcon <FAIcon
@@ -21,7 +22,7 @@
v-if="unreadChatCount" v-if="unreadChatCount"
class="alert-dot" class="alert-dot"
/> />
</button> </a>
<router-link <router-link
v-if="!hideSitename" v-if="!hideSitename"
class="site-name" class="site-name"
@@ -32,9 +33,10 @@
</router-link> </router-link>
</div> </div>
<div class="item right"> <div class="item right">
<button <a
v-if="currentUser" v-if="currentUser"
class="button-unstyled mobile-nav-button" class="mobile-nav-button"
href="#"
@click.stop.prevent="openMobileNotifications()" @click.stop.prevent="openMobileNotifications()"
> >
<FAIcon <FAIcon
@@ -45,7 +47,7 @@
v-if="unseenNotificationsCount" v-if="unseenNotificationsCount"
class="alert-dot" class="alert-dot"
/> />
</button> </a>
</div> </div>
</nav> </nav>
<div <div
@@ -1,7 +1,7 @@
<template> <template>
<div v-if="isLoggedIn"> <div v-if="isLoggedIn">
<button <button
class="button-default new-status-button" class="new-status-button"
:class="{ 'hidden': isHidden }" :class="{ 'hidden': isHidden }"
@click="openPostForm" @click="openPostForm"
> >
@@ -1,11 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
import DialogModal from '../dialog_modal/dialog_modal.vue' import DialogModal from '../dialog_modal/dialog_modal.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
library.add(faChevronDown)
const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip' const STRIP_MEDIA = 'mrf_tag:media-strip'
const FORCE_UNLISTED = 'mrf_tag:force-unlisted' const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
@@ -8,17 +8,17 @@
@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
class="button-default dropdown-item" class="dropdown-item"
@click="toggleRight(&quot;admin&quot;)" @click="toggleRight(&quot;admin&quot;)"
> >
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleRight(&quot;moderator&quot;)" @click="toggleRight(&quot;moderator&quot;)"
> >
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
@@ -29,13 +29,13 @@
/> />
</span> </span>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleActivationStatus()" @click="toggleActivationStatus()"
> >
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }} {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="deleteUserDialog(true)" @click="deleteUserDialog(true)"
> >
{{ $t('user_card.admin_menu.delete_account') }} {{ $t('user_card.admin_menu.delete_account') }}
@@ -47,109 +47,107 @@
/> />
<span v-if="hasTagPolicy"> <span v-if="hasTagPolicy">
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)" @click="toggleTag(tags.FORCE_NSFW)"
> >
{{ $t('user_card.admin_menu.force_nsfw') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/> />
{{ $t('user_card.admin_menu.force_nsfw') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.STRIP_MEDIA)" @click="toggleTag(tags.STRIP_MEDIA)"
> >
{{ $t('user_card.admin_menu.strip_media') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/> />
{{ $t('user_card.admin_menu.strip_media') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.FORCE_UNLISTED)" @click="toggleTag(tags.FORCE_UNLISTED)"
> >
{{ $t('user_card.admin_menu.force_unlisted') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/> />
{{ $t('user_card.admin_menu.force_unlisted') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.SANDBOX)" @click="toggleTag(tags.SANDBOX)"
> >
{{ $t('user_card.admin_menu.sandbox') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/> />
{{ $t('user_card.admin_menu.sandbox') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
> >
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
/> />
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
> >
{{ $t('user_card.admin_menu.disable_any_subscription') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
/> />
{{ $t('user_card.admin_menu.disable_any_subscription') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="dropdown-item"
@click="toggleTag(tags.QUARANTINE)" @click="toggleTag(tags.QUARANTINE)"
> >
{{ $t('user_card.admin_menu.quarantine') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/> />
{{ $t('user_card.admin_menu.quarantine') }}
</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 btn-default btn-block"
:class="{ toggled }" :class="{ toggled }"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" /> </button>
</button>
</template>
</Popover> </Popover>
<portal 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 btn-default"
@click="deleteUserDialog(false)" @click="deleteUserDialog(false)"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </button>
<button <button
class="btn button-default danger" class="btn btn-default danger"
@click="deleteUser()" @click="deleteUser()"
> >
{{ $t('user_card.admin_menu.delete_user') }} {{ $t('user_card.admin_menu.delete_user') }}
@@ -165,6 +163,25 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.menu-checkbox {
float: right;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
line-height: 22px;
text-align: center;
border-radius: 0px;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
&.menu-checkbox-checked::after {
content: '✓';
}
}
.moderation-tools-popover { .moderation-tools-popover {
height: 100%; height: 100%;
.trigger { .trigger {
@@ -172,10 +189,4 @@
height: 100%; height: 100%;
} }
} }
.moderation-tools-button {
svg,i {
font-size: 0.8em;
}
}
</style> </style>
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="mute-card-content-container"> <div class="mute-card-content-container">
<button <button
v-if="muted" v-if="muted"
class="btn button-default" class="btn btn-default"
:disabled="progress" :disabled="progress"
@click="unmuteUser" @click="unmuteUser"
> >
@@ -16,7 +16,7 @@
</button> </button>
<button <button
v-else v-else
class="btn button-default" class="btn btn-default"
:disabled="progress" :disabled="progress"
@click="muteUser" @click="muteUser"
> >
+14 -22
View File
@@ -1,4 +1,4 @@
import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' import { timelineNames } from '../timeline_menu/timeline_menu.js'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@@ -7,12 +7,10 @@ import {
faGlobe, faGlobe,
faBookmark, faBookmark,
faEnvelope, faEnvelope,
faChevronDown, faHome,
faChevronUp,
faComments, faComments,
faBell, faBell,
faInfoCircle, faInfoCircle
faStream
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@@ -20,12 +18,10 @@ library.add(
faGlobe, faGlobe,
faBookmark, faBookmark,
faEnvelope, faEnvelope,
faChevronDown, faHome,
faChevronUp,
faComments, faComments,
faBell, faBell,
faInfoCircle, faInfoCircle
faStream
) )
const NavPanel = { const NavPanel = {
@@ -34,20 +30,16 @@ const NavPanel = {
this.$store.dispatch('startFetchingFollowRequests') this.$store.dispatch('startFetchingFollowRequests')
} }
}, },
components: {
TimelineMenuContent
},
data () {
return {
showTimelines: false
}
},
methods: {
toggleTimelines () {
this.showTimelines = !this.showTimelines
}
},
computed: { computed: {
onTimelineRoute () {
return !!timelineNames()[this.$route.name]
},
timelinesRoute () {
if (this.$store.state.interface.lastTimeline) {
return this.$store.state.interface.lastTimeline
}
return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length, followRequestCount: state => state.api.followRequests.length,
+13 -57
View File
@@ -3,33 +3,19 @@
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if="currentUser || !privateMode"> <li v-if="currentUser || !privateMode">
<button <router-link
class="button-unstyled menu-item" :to="{ name: timelinesRoute }"
@click="toggleTimelines" :class="onTimelineRoute && 'router-link-active'"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
icon="stream" icon="home"
/>{{ $t("nav.timelines") }} />{{ $t("nav.timelines") }}
<FAIcon </router-link>
class="timelines-chevron"
fixed-width
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
/>
</button>
<div
v-show="showTimelines"
class="timelines-background"
>
<TimelineMenuContent class="timelines" />
</div>
</li> </li>
<li v-if="currentUser"> <li v-if="currentUser">
<router-link <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
class="menu-item"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@@ -38,10 +24,7 @@
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && pleromaChatMessagesAvailable"> <li v-if="currentUser && pleromaChatMessagesAvailable">
<router-link <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
class="menu-item"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<div <div
v-if="unreadChatCount" v-if="unreadChatCount"
class="badge badge-notification" class="badge badge-notification"
@@ -56,10 +39,7 @@
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && currentUser.locked"> <li v-if="currentUser && currentUser.locked">
<router-link <router-link :to="{ name: 'friend-requests' }">
class="menu-item"
:to="{ name: 'friend-requests' }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@@ -74,10 +54,7 @@
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link <router-link :to="{ name: 'about' }">
class="menu-item"
:to="{ name: 'about' }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@@ -114,14 +91,14 @@
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
padding: 0; padding: 0;
&:first-child .menu-item { &:first-child a {
border-top-right-radius: $fallback--panelRadius; border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius; border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius); border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
} }
&:last-child .menu-item { &:last-child a {
border-bottom-right-radius: $fallback--panelRadius; border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius; border-bottom-left-radius: $fallback--panelRadius;
@@ -133,15 +110,13 @@
border: none; border: none;
} }
.menu-item { a {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
align-items: stretch;
height: 3.5em; height: 3.5em;
line-height: 3.5em; line-height: 3.5em;
padding: 0 1em; padding: 0 1em;
width: 100%;
color: $fallback--link;
color: var(--link, $fallback--link);
&:hover { &:hover {
background-color: $fallback--lightBg; background-color: $fallback--lightBg;
@@ -171,25 +146,6 @@
} }
} }
.timelines-chevron {
margin-left: 0.8em;
font-size: 1.1em;
}
.timelines-background {
padding: 0 0 0 0.6em;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.timelines {
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.fa-scale-110 { .fa-scale-110 {
margin-right: 0.8em; margin-right: 0.8em;
} }
+18 -21
View File
@@ -11,18 +11,17 @@
> >
<small> <small>
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
{{ notification.from_profile.screen_name_ui }} {{ notification.from_profile.screen_name }}
</router-link> </router-link>
</small> </small>
<button <a
class="button-unstyled unmute" href="#"
class="unmute"
@click.prevent="toggleMute" @click.prevent="toggleMute"
> ><FAIcon
<FAIcon class="fa-scale-110 fa-old-padding"
class="fa-scale-110 fa-old-padding" icon="eye-slash"
icon="eye-slash" /></a>
/>
</button>
</div> </div>
<div <div
v-else v-else
@@ -54,14 +53,14 @@
<bdi <bdi
v-if="!!notification.from_profile.name_html" v-if="!!notification.from_profile.name_html"
class="username" class="username"
:title="'@'+notification.from_profile.screen_name_ui" :title="'@'+notification.from_profile.screen_name"
v-html="notification.from_profile.name_html" v-html="notification.from_profile.name_html"
/> />
<!-- 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"
>{{ notification.from_profile.name }}</span> >{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'"> <span v-if="notification.type === 'like'">
<FAIcon <FAIcon
@@ -133,16 +132,14 @@
/> />
</span> </span>
</div> </div>
<button <a
v-if="needMute" v-if="needMute"
class="button-unstyled" href="#"
@click.prevent="toggleMute" @click.prevent="toggleMute"
> ><FAIcon
<FAIcon class="fa-scale-110 fa-old-padding"
class="fa-scale-110 fa-old-padding" icon="eye-slash"
icon="eye-slash" /></a>
/>
</button>
</span> </span>
<div <div
v-if="notification.type === 'follow' || notification.type === 'follow_request'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
@@ -152,7 +149,7 @@
:to="userProfileLink" :to="userProfileLink"
class="follow-name" class="follow-name"
> >
@{{ notification.from_profile.screen_name_ui }} @{{ notification.from_profile.screen_name }}
</router-link> </router-link>
<div <div
v-if="notification.type === 'follow_request'" v-if="notification.type === 'follow_request'"
@@ -177,7 +174,7 @@
class="move-text" class="move-text"
> >
<router-link :to="targetUserProfileLink"> <router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name_ui }} @{{ notification.target.screen_name }}
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>
@@ -1,122 +0,0 @@
<template>
<Popover
trigger="click"
class="NotificationFilters"
placement="bottom"
:bound-to="{ x: 'container' }"
>
<template v-slot:content>
<div class="dropdown-menu">
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('likes')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.likes }"
/>{{ $t('settings.notification_visibility_likes') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('repeats')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.repeats }"
/>{{ $t('settings.notification_visibility_repeats') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('follows')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.follows }"
/>{{ $t('settings.notification_visibility_follows') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('mentions')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.mentions }"
/>{{ $t('settings.notification_visibility_mentions') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('emojiReactions')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.emojiReactions }"
/>{{ $t('settings.notification_visibility_emoji_reactions') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('moves')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.moves }"
/>{{ $t('settings.notification_visibility_moves') }}
</button>
</div>
</template>
<template v-slot:trigger>
<button class="button-unstyled">
<FAIcon icon="filter" />
</button>
</template>
</Popover>
</template>
<script>
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
library.add(
faFilter
)
export default {
components: { Popover },
computed: {
filters () {
return this.$store.getters.mergedConfig.notificationVisibility
}
},
methods: {
toggleNotificationFilter (type) {
this.$store.dispatch('setOption', {
name: 'notificationVisibility',
value: {
...this.filters,
[type]: !this.filters[type]
}
})
}
}
}
</script>
<style lang="scss">
.NotificationFilters {
align-self: stretch;
> button {
font-size: 1.2em;
padding-left: 0.7em;
padding-right: 0.2em;
line-height: 100%;
height: 100%;
}
.dropdown-item {
margin: 0;
}
}
</style>
@@ -1,13 +1,11 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Notification from '../notification/notification.vue' import Notification from '../notification/notification.vue'
import NotificationFilters from './notification_filters.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import { import {
notificationsFromStore, notificationsFromStore,
filteredNotificationsFromStore, filteredNotificationsFromStore,
unseenNotificationsFromStore unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js' } from '../../services/notification_utils/notification_utils.js'
import FaviconService from '../../services/favicon_service/favicon_service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@@ -18,10 +16,6 @@ library.add(
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = { const Notifications = {
components: {
Notification,
NotificationFilters
},
props: { props: {
// Disables display of panel header // Disables display of panel header
noHeading: Boolean, noHeading: Boolean,
@@ -40,6 +34,11 @@ const Notifications = {
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
} }
}, },
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.fetchAndUpdate({ store, credentials })
},
computed: { computed: {
mainClass () { mainClass () {
return this.minimalMode ? '' : 'panel panel-default' return this.minimalMode ? '' : 'panel panel-default'
@@ -70,13 +69,14 @@ const Notifications = {
}, },
...mapGetters(['unreadChatCount']) ...mapGetters(['unreadChatCount'])
}, },
components: {
Notification
},
watch: { watch: {
unseenCountTitle (count) { unseenCountTitle (count) {
if (count > 0) { if (count > 0) {
FaviconService.drawFaviconBadge()
this.$store.dispatch('setPageTitle', `(${count})`) this.$store.dispatch('setPageTitle', `(${count})`)
} else { } else {
FaviconService.clearFaviconBadge()
this.$store.dispatch('setPageTitle', '') this.$store.dispatch('setPageTitle', '')
} }
} }
@@ -1,6 +1,6 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.Notifications { .notifications {
&:not(.minimal) { &:not(.minimal) {
// a bit of a hack to allow scrolling below notifications // a bit of a hack to allow scrolling below notifications
padding-bottom: 15em; padding-bottom: 15em;
@@ -11,10 +11,6 @@
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
.notifications-footer {
border: none;
}
.notification { .notification {
position: relative; position: relative;
@@ -86,6 +82,7 @@
} }
} }
.follow-text, .move-text { .follow-text, .move-text {
padding: 0.5em 0; padding: 0.5em 0;
overflow-wrap: break-word; overflow-wrap: break-word;
+16 -10
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <div
:class="{ minimal: minimalMode }" :class="{ minimal: minimalMode }"
class="Notifications" class="notifications"
> >
<div :class="mainClass"> <div :class="mainClass">
<div <div
@@ -15,14 +15,20 @@
class="badge badge-notification unseen-count" class="badge badge-notification unseen-count"
>{{ unseenCount }}</span> >{{ unseenCount }}</span>
</div> </div>
<div
v-if="error"
class="loadmore-error alert error"
@click.prevent
>
{{ $t('timeline.error_fetching') }}
</div>
<button <button
v-if="unseenCount" v-if="unseenCount"
class="button-default read-button" class="read-button"
@click.prevent="markAsSeen" @click.prevent="markAsSeen"
> >
{{ $t('notifications.read') }} {{ $t('notifications.read') }}
</button> </button>
<NotificationFilters />
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div <div
@@ -35,25 +41,25 @@
<notification :notification="notification" /> <notification :notification="notification" />
</div> </div>
</div> </div>
<div class="panel-footer notifications-footer"> <div class="panel-footer">
<div <div
v-if="bottomedOut" v-if="bottomedOut"
class="new-status-notification text-center faint" class="new-status-notification text-center panel-footer faint"
> >
{{ $t('notifications.no_more_notifications') }} {{ $t('notifications.no_more_notifications') }}
</div> </div>
<button <a
v-else-if="!loading" v-else-if="!loading"
class="button-unstyled -link -fullwidth" href="#"
@click.prevent="fetchOlderNotifications()" @click.prevent="fetchOlderNotifications()"
> >
<div class="new-status-notification text-center"> <div class="new-status-notification text-center panel-footer">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div> </div>
</button> </a>
<div <div
v-else v-else
class="new-status-notification text-center" class="new-status-notification text-center panel-footer"
> >
<FAIcon <FAIcon
icon="circle-notch" icon="circle-notch"
@@ -51,9 +51,9 @@
<button <button
:disabled="isPending" :disabled="isPending"
type="submit" type="submit"
class="btn button-default btn-block" class="btn btn-default btn-block"
> >
{{ $t('settings.save') }} {{ $t('general.submit') }}
</button> </button>
</div> </div>
</div> </div>
+3 -9
View File
@@ -42,15 +42,14 @@
:value="index" :value="index"
> >
<label class="option-vote"> <label class="option-vote">
<!-- eslint-disable-next-line vue/no-v-html --> <div>{{ option.title }}</div>
<div v-html="option.title_html" />
</label> </label>
</div> </div>
</div> </div>
<div class="footer faint"> <div class="footer faint">
<button <button
v-if="!showResults" v-if="!showResults"
class="btn button-default poll-vote-button" class="btn btn-default poll-vote-button"
type="button" type="button"
:disabled="isDisabled" :disabled="isDisabled"
@click="vote" @click="vote"
@@ -58,12 +57,7 @@
{{ $t('polls.vote') }} {{ $t('polls.vote') }}
</button> </button>
<div class="total"> <div class="total">
<template v-if="typeof poll.voters_count === 'number'"> {{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp;
{{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp;
</template>
<template v-else>
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template>
</div> </div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<Timeago <Timeago
+2 -4
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: () => ({
+64 -37
View File
@@ -21,17 +21,20 @@
@keydown.enter.stop.prevent="nextOption(index)" @keydown.enter.stop.prevent="nextOption(index)"
> >
</div> </div>
<button <div
v-if="options.length > 2" v-if="options.length > 2"
class="delete-option button-unstyled -hover-highlight" class="icon-container"
@click="deleteOption(index)"
> >
<FAIcon icon="times" /> <FAIcon
</button> icon="times"
class="delete"
@click="deleteOption(index)"
/>
</div>
</div> </div>
<button <a
v-if="options.length < maxOptions" v-if="options.length < maxOptions"
class="add-option faint button-unstyled -hover-highlight" class="add-option faint"
@click="addOption" @click="addOption"
> >
<FAIcon <FAIcon
@@ -40,25 +43,29 @@
/> />
{{ $t("polls.add_option") }} {{ $t("polls.add_option") }}
</button> </a>
<div class="poll-type-expiry"> <div class="poll-type-expiry">
<div <div
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,20 +79,24 @@
:max="maxExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit"
@change="expiryAmountChange" @change="expiryAmountChange"
> >
<Select <label class="expiry-unit select">
v-model="expiryUnit" <select
unstyled="true" v-model="expiryUnit"
class="expiry-unit" @change="expiryAmountChange"
@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>
@@ -105,6 +116,7 @@
align-self: flex-start; align-self: flex-start;
padding-top: 0.25em; padding-top: 0.25em;
padding-left: 0.1em; padding-left: 0.1em;
cursor: pointer;
} }
.poll-option { .poll-option {
@@ -123,11 +135,19 @@
} }
} }
.delete-option { .icon-container {
// Hack: Move the icon over the input box // Hack: Move the icon over the input box
width: 1.5em; width: 1.5em;
margin-left: -1.5em; margin-left: -1.5em;
z-index: 1; z-index: 1;
.delete {
cursor: pointer;
&:hover {
color: inherit;
}
}
} }
.poll-type-expiry { .poll-type-expiry {
@@ -139,9 +159,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;
padding-right: 0.75em; box-shadow: none;
background-color: transparent;
} }
} }
@@ -152,6 +173,12 @@
width: 3em; width: 3em;
text-align: right; text-align: right;
} }
.expiry-unit {
border: none;
box-shadow: none;
background-color: transparent;
}
} }
} }
</style> </style>
+6 -28
View File
@@ -3,35 +3,25 @@ const Popover = {
props: { props: {
// Action to trigger popover: either 'hover' or 'click' // Action to trigger popover: either 'hover' or 'click'
trigger: String, trigger: String,
// Either 'top' or 'bottom' // Either 'top' or 'bottom'
placement: String, placement: String,
// Takes object with properties 'x' and 'y', values of these can be // Takes object with properties 'x' and 'y', values of these can be
// 'container' for using offsetParent as boundaries for either axis // 'container' for using offsetParent as boundaries for either axis
// or 'viewport' // or 'viewport'
boundTo: Object, boundTo: Object,
// Takes a selector to use as a replacement for the parent container // Takes a selector to use as a replacement for the parent container
// for getting boundaries for x an y axis // for getting boundaries for x an y axis
boundToSelector: String, boundToSelector: String,
// Takes a top/bottom/left/right object, how much space to leave // Takes a top/bottom/left/right object, how much space to leave
// between boundary and popover element // between boundary and popover element
margin: Object, margin: Object,
// Takes a x/y object and tells how many pixels to offset from // Takes a x/y object and tells how many pixels to offset from
// anchor point on either axis // anchor point on either axis
offset: Object, offset: Object,
// Replaces the classes you may want for the popover container. // Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover // Use 'popover-default' in addition to get the default popover
// styles with your custom class. // styles with your custom class.
popoverClass: String, popoverClass: String
// If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom.
removePadding: Boolean
}, },
data () { data () {
return { return {
@@ -54,11 +44,8 @@ const Popover = {
} }
// Popover will be anchored around this element, trigger ref is the container, so // Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one v-slot:trigger. // its children are what are inside the slot. Expect only one slot="trigger".
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const screenBox = anchorEl.getBoundingClientRect() const screenBox = anchorEl.getBoundingClientRect()
// Screen position of the origin point for popover // Screen position of the origin point for popover
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top } const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
@@ -109,19 +96,13 @@ const Popover = {
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
let vPadding = 0
if (this.removePadding && usingTop) {
const anchorStyle = getComputedStyle(anchorEl)
vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
}
const yOffset = (this.offset && this.offset.y) || 0 const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop const translateY = usingTop
? -anchorHeight + vPadding - yOffset - content.offsetHeight ? -anchorEl.offsetHeight - yOffset - content.offsetHeight
: yOffset : yOffset
const xOffset = (this.offset && this.offset.x) || 0 const xOffset = (this.offset && this.offset.x) || 0
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
// Note, separate translateX and translateY avoids blurry text on chromium, // Note, separate translateX and translateY avoids blurry text on chromium,
// single translate or translate3d resulted in blurry text. // single translate or translate3d resulted in blurry text.
@@ -131,12 +112,9 @@ const Popover = {
} }
}, },
showPopover () { showPopover () {
const wasHidden = this.hidden if (this.hidden) this.$emit('show')
this.hidden = false this.hidden = false
this.$nextTick(() => { this.$nextTick(this.updateStyles)
if (wasHidden) this.$emit('show')
this.updateStyles()
})
}, },
hidePopover () { hidePopover () {
if (!this.hidden) this.$emit('close') if (!this.hidden) this.$emit('close')
+7 -39
View File
@@ -3,14 +3,12 @@
@mouseenter="onMouseenter" @mouseenter="onMouseenter"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
<button <div
ref="trigger" ref="trigger"
class="button-unstyled -fullwidth popover-trigger-button"
type="button"
@click="onClick" @click="onClick"
> >
<slot name="trigger" /> <slot name="trigger" />
</button> </div>
<div <div
v-if="!hidden" v-if="!hidden"
ref="content" ref="content"
@@ -32,10 +30,6 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.popover-trigger-button {
display: block;
}
.popover { .popover {
z-index: 8; z-index: 8;
position: absolute; position: absolute;
@@ -82,9 +76,10 @@
.dropdown-item { .dropdown-item {
line-height: 21px; line-height: 21px;
margin-right: 5px;
overflow: auto; overflow: auto;
display: block; display: block;
padding: .5em 0.75em; padding: .25rem 1.0rem .25rem 1.5rem;
clear: both; clear: both;
font-weight: 400; font-weight: 400;
text-align: inherit; text-align: inherit;
@@ -95,14 +90,14 @@
box-shadow: none; box-shadow: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
box-sizing: border-box;
--btnText: var(--popoverText, $fallback--text); --btnText: var(--popoverText, $fallback--text);
&-icon { &-icon {
padding-left: 0.5rem;
svg { svg {
width: 22px; margin-right: 0.25rem;
margin-right: 0.75rem;
color: var(--menuPopoverIcon, $fallback--icon) color: var(--menuPopoverIcon, $fallback--icon)
} }
} }
@@ -121,33 +116,6 @@
} }
} }
.menu-checkbox {
display: inline-block;
vertical-align: middle;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
line-height: 22px;
text-align: center;
border-radius: 0px;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
margin-right: 0.75em;
&.menu-checkbox-checked::after {
font-size: 1.25em;
content: '✓';
}
&.menu-checkbox-radio::after {
font-size: 2em;
content: '•';
}
}
} }
} }
</style> </style>
@@ -11,10 +11,10 @@ import { reject, map, uniqBy, debounce } from 'lodash'
import suggestor from '../emoji_input/suggestor.js' import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faChevronDown,
faSmileBeam, faSmileBeam,
faPollH, faPollH,
faUpload, faUpload,
@@ -24,6 +24,7 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faChevronDown,
faSmileBeam, faSmileBeam,
faPollH, faPollH,
faUpload, faUpload,
@@ -83,7 +84,6 @@ const PostStatusForm = {
PollForm, PollForm,
ScopeSelector, ScopeSelector,
Checkbox, Checkbox,
Select,
Attachment, Attachment,
StatusContent StatusContent
}, },
@@ -115,7 +115,7 @@ const PostStatusForm = {
? this.copyMessageScope ? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope : this.$store.state.users.currentUser.default_scope
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig const { postContentType: contentType } = this.$store.getters.mergedConfig
return { return {
dropFiles: [], dropFiles: [],
@@ -126,7 +126,7 @@ const PostStatusForm = {
newStatus: { newStatus: {
spoilerText: this.subject || '', spoilerText: this.subject || '',
status: statusText, status: statusText,
nsfw: !!sensitiveByDefault, nsfw: false,
files: [], files: [],
poll: {}, poll: {},
mediaDescriptions: {}, mediaDescriptions: {},
@@ -159,7 +159,8 @@ const PostStatusForm = {
...this.$store.state.instance.emoji, ...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji ...this.$store.state.instance.customEmoji
], ],
store: this.$store users: this.$store.state.users.users,
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
}) })
}, },
emojiSuggestor () { emojiSuggestor () {
@@ -530,7 +531,7 @@ const PostStatusForm = {
!(isFormBiggerThanScroller && !(isFormBiggerThanScroller &&
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length) this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0 const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
const targetScroll = Math.round(currentScroll + totalDelta) const targetScroll = currentScroll + totalDelta
if (scrollerRef === window) { if (scrollerRef === window) {
scrollerRef.scroll(0, targetScroll) scrollerRef.scroll(0, targetScroll)
@@ -24,12 +24,12 @@
tag="p" tag="p"
class="visibility-notice" class="visibility-notice"
> >
<button <a
class="button-unstyled -link" href="#"
@click="openProfileTab" @click="openProfileTab"
> >
{{ $t('post_status.account_not_locked_warning_link') }} {{ $t('post_status.account_not_locked_warning_link') }}
</button> </a>
</i18n> </i18n>
<p <p
v-if="!hideScopeNotice && newStatus.visibility === 'public'" v-if="!hideScopeNotice && newStatus.visibility === 'public'"
@@ -189,19 +189,28 @@
v-if="postFormats.length > 1" v-if="postFormats.length > 1"
class="text-format" class="text-format"
> >
<Select <label
id="post-content-type" for="post-content-type"
v-model="newStatus.contentType" class="select"
class="form-control"
> >
<option <select
v-for="postFormat in postFormats" id="post-content-type"
:key="postFormat" v-model="newStatus.contentType"
:value="postFormat" class="form-control"
> >
{{ $t(`post_status.content_type["${postFormat}"]`) }} <option
</option> v-for="postFormat in postFormats"
</Select> :key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div> </div>
<div <div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
@@ -234,46 +243,50 @@
@upload-failed="uploadFailed" @upload-failed="uploadFailed"
@all-uploaded="finishedUploadingFiles" @all-uploaded="finishedUploadingFiles"
/> />
<button <div
class="emoji-icon button-unstyled" class="emoji-icon"
:title="$t('emoji.add_emoji')"
@click="showEmojiPicker"
> >
<FAIcon icon="smile-beam" /> <div
</button> :title="$t('emoji.add_emoji')"
<button class="btn btn-default"
@click="showEmojiPicker"
>
<FAIcon icon="smile-beam" />
</div>
</div>
<div
v-if="pollsAvailable" v-if="pollsAvailable"
class="poll-icon button-unstyled" class="poll-icon"
:class="{ selected: pollFormVisible }" :class="{ selected: pollFormVisible }"
:title="$t('polls.add_poll')" :title="$t('polls.add_poll')"
@click="togglePollForm" @click="togglePollForm"
> >
<FAIcon icon="poll-h" /> <FAIcon icon="poll-h" />
</button> </div>
</div> </div>
<button <button
v-if="posting" v-if="posting"
disabled disabled
class="btn button-default" class="btn btn-default"
> >
{{ $t('post_status.posting') }} {{ $t('post_status.posting') }}
</button> </button>
<button <button
v-else-if="isOverLengthLimit" v-else-if="isOverLengthLimit"
disabled disabled
class="btn button-default" class="btn btn-default"
> >
{{ $t('post_status.post') }} {{ $t('general.submit') }}
</button> </button>
<!-- touchstart is used to keep the OSK at the same position after a message send --> <!-- touchstart is used to keep the OSK at the same position after a message send -->
<button <button
v-else v-else
:disabled="uploadingFiles || disableSubmit" :disabled="uploadingFiles || disableSubmit"
class="btn button-default" class="btn btn-default"
@touchstart.stop.prevent="postStatus($event, newStatus)" @touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)"
> >
{{ $t('post_status.post') }} {{ $t('general.submit') }}
</button> </button>
</div> </div>
<div <div
@@ -293,12 +306,11 @@
:key="file.url" :key="file.url"
class="media-upload-wrapper" class="media-upload-wrapper"
> >
<button <FAIcon
class="button-unstyled hider" class="fa-scale-110 fa-old-padding"
icon="times"
@click="removeMediaFile(file)" @click="removeMediaFile(file)"
> />
<FAIcon icon="times" />
</button>
<attachment <attachment
:attachment="file" :attachment="file"
:set-media="() => $store.dispatch('setMedia', newStatus.files)" :set-media="() => $store.dispatch('setMedia', newStatus.files)"
@@ -508,11 +520,26 @@
} }
.attachments .media-upload-wrapper { .attachments .media-upload-wrapper {
position: relative; padding: 0 0.5em;
.attachment { .attachment {
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative;
}
.fa-scale-110 fa-old-padding {
position: absolute;
margin: 10px;
margin: .75em;
padding: .5em;
background: rgba(230,230,230,0.6);
z-index: 2;
color: black;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
font-weight: bold;
cursor: pointer;
} }
} }
+1 -15
View File
@@ -23,31 +23,17 @@ const ReactButton = {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
} }
close() close()
},
focusInput () {
this.$nextTick(() => {
const input = this.$el.querySelector('input')
if (input) input.focus()
})
} }
}, },
computed: { computed: {
commonEmojis () { commonEmojis () {
return [ return ['👍', '😠', '👀', '😂', '🔥']
{ displayText: 'thumbsup', replacement: '👍' },
{ displayText: 'angry', replacement: '😠' },
{ displayText: 'eyes', replacement: '👀' },
{ displayText: 'joy', replacement: '😂' },
{ displayText: 'fire', replacement: '🔥' }
]
}, },
emojis () { emojis () {
if (this.filterWord !== '') { if (this.filterWord !== '') {
const filterWordLowercase = this.filterWord.toLowerCase() const filterWordLowercase = this.filterWord.toLowerCase()
let orderedEmojiList = [] let orderedEmojiList = []
for (const emoji of this.$store.state.instance.emoji) { for (const emoji of this.$store.state.instance.emoji) {
if (emoji.replacement === this.filterWord) return [emoji]
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
if (indexOfFilterWord > -1) { if (indexOfFilterWord > -1) {
if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {
+62 -79
View File
@@ -1,14 +1,14 @@
<template> <template>
<Popover <Popover
trigger="click" trigger="click"
class="ReactButton"
placement="top" placement="top"
:offset="{ y: 5 }" :offset="{ y: 5 }"
:bound-to="{ x: 'container' }" class="react-button-popover"
remove-padding
@show="focusInput"
> >
<template v-slot:content="{close}"> <div
slot="content"
slot-scope="{close}"
>
<div class="reaction-picker-filter"> <div class="reaction-picker-filter">
<input <input
v-model="filterWord" v-model="filterWord"
@@ -19,37 +19,30 @@
<div class="reaction-picker"> <div class="reaction-picker">
<span <span
v-for="emoji in commonEmojis" v-for="emoji in commonEmojis"
:key="emoji.replacement" :key="emoji"
class="emoji-button" class="emoji-button"
:title="emoji.displayText" @click="addReaction($event, emoji, close)"
@click="addReaction($event, emoji.replacement, close)"
> >
{{ emoji.replacement }} {{ emoji }}
</span> </span>
<div class="reaction-picker-divider" /> <div class="reaction-picker-divider" />
<span <span
v-for="(emoji, key) in emojis" v-for="(emoji, key) in emojis"
:key="key" :key="key"
class="emoji-button" class="emoji-button"
:title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)" @click="addReaction($event, emoji.replacement, close)"
> >
{{ emoji.replacement }} {{ emoji.replacement }}
</span> </span>
<div class="reaction-bottom-fader" /> <div class="reaction-bottom-fader" />
</div> </div>
</template> </div>
<template v-slot:trigger> <FAIcon
<button slot="trigger"
class="button-unstyled popover-trigger" class="fa-scale-110 fa-old-padding add-reaction-button"
:title="$t('tool_tip.add_reaction')" :icon="['far', 'smile-beam']"
> :title="$t('tool_tip.add_reaction')"
<FAIcon />
class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']"
/>
</button>
</template>
</Popover> </Popover>
</template> </template>
@@ -58,72 +51,62 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.ReactButton { .reaction-picker-filter {
.reaction-picker-filter { padding: 0.5em;
padding: 0.5em; display: flex;
display: flex; input {
flex: 1;
input {
flex: 1;
}
} }
}
.reaction-picker-divider { .reaction-picker-divider {
height: 1px; height: 1px;
width: 100%; width: 100%;
margin: 0.5em; margin: 0.5em;
background-color: var(--border, $fallback--border); background-color: var(--border, $fallback--border);
} }
.reaction-picker { .reaction-picker {
width: 10em; width: 10em;
height: 9em; height: 9em;
font-size: 1.5em; font-size: 1.5em;
overflow-y: scroll; overflow-y: scroll;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0.5em; padding: 0.5em;
text-align: center; text-align: center;
align-content: flex-start; align-content: flex-start;
user-select: none; user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
transition: mask-size 150ms; transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto; mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
/* Autoprefixed seem to ignore this one, and also syntax is different */ .emoji-button {
-webkit-mask-composite: xor; cursor: pointer;
mask-composite: exclude;
.emoji-button { flex-basis: 20%;
cursor: pointer; line-height: 1.5em;
align-content: center;
flex-basis: 20%; &:hover {
line-height: 1.5em; transform: scale(1.25);
align-content: center;
&:hover {
transform: scale(1.25);
}
}
}
/* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
.popover-trigger {
padding: 10px;
margin: -10px;
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
} }
} }
} }
.add-reaction-button {
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style> </style>
+4 -13
View File
@@ -10,8 +10,7 @@ const registration = {
fullname: '', fullname: '',
username: '', username: '',
password: '', password: '',
confirm: '', confirm: ''
reason: ''
}, },
captcha: {} captcha: {}
}), }),
@@ -25,8 +24,7 @@ const registration = {
confirm: { confirm: {
required, required,
sameAsPassword: sameAs('password') sameAsPassword: sameAs('password')
}, }
reason: { required: requiredIf(() => this.accountApprovalRequired) }
} }
} }
}, },
@@ -40,10 +38,7 @@ const registration = {
computed: { computed: {
token () { return this.$route.params.token }, token () { return this.$route.params.token },
bioPlaceholder () { bioPlaceholder () {
return this.replaceNewlines(this.$t('registration.bio_placeholder')) return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n')
},
reasonPlaceholder () {
return this.replaceNewlines(this.$t('registration.reason_placeholder'))
}, },
...mapState({ ...mapState({
registrationOpen: (state) => state.instance.registrationOpen, registrationOpen: (state) => state.instance.registrationOpen,
@@ -51,8 +46,7 @@ const registration = {
isPending: (state) => state.users.signUpPending, isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors, serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos, termsOfService: (state) => state.instance.tos,
accountActivationRequired: (state) => state.instance.accountActivationRequired, accountActivationRequired: (state) => state.instance.accountActivationRequired
accountApprovalRequired: (state) => state.instance.accountApprovalRequired
}) })
}, },
methods: { methods: {
@@ -79,9 +73,6 @@ const registration = {
}, },
setCaptcha () { setCaptcha () {
this.getCaptcha().then(cpt => { this.captcha = cpt }) this.getCaptcha().then(cpt => { this.captcha = cpt })
},
replaceNewlines (str) {
return str.replace(/\s*\n\s*/g, ' \n')
} }
} }
} }
+2 -19
View File
@@ -162,23 +162,6 @@
</ul> </ul>
</div> </div>
<div
v-if="accountApprovalRequired"
class="form-group"
>
<label
class="form--label"
for="reason"
>{{ $t('registration.reason') }}</label>
<textarea
id="reason"
v-model="user.reason"
:disabled="isPending"
class="form-control"
:placeholder="reasonPlaceholder"
/>
</div>
<div <div
v-if="captcha.type != 'none'" v-if="captcha.type != 'none'"
id="captcha-group" id="captcha-group"
@@ -228,9 +211,9 @@
<button <button
:disabled="isPending" :disabled="isPending"
type="submit" type="submit"
class="btn button-default" class="btn btn-default"
> >
{{ $t('registration.register') }} {{ $t('general.submit') }}
</button> </button>
</div> </div>
</div> </div>
@@ -16,7 +16,7 @@
> >
<button <button
click="submit" click="submit"
class="button-default remote-button" class="remote-button"
> >
{{ $t('user_card.remote_follow') }} {{ $t('user_card.remote_follow') }}
</button> </button>
+17 -36
View File
@@ -1,28 +1,20 @@
<template> <template>
<div class="ReplyButton"> <div>
<button <FAIcon
v-if="loggedIn" v-if="loggedIn"
class="button-unstyled interactive" class="ReplyButton fa-scale-110 fa-old-padding -interactive"
:class="{'-active': replying}" icon="reply"
:title="$t('tool_tip.reply')" :title="$t('tool_tip.reply')"
:class="{'-active': replying}"
@click.prevent="$emit('toggle')" @click.prevent="$emit('toggle')"
> />
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" v-else
icon="reply" icon="reply"
/> class="ReplyButton fa-scale-110 fa-old-padding"
</button> :title="$t('tool_tip.reply')"
<span v-else> />
<FAIcon <span v-if="status.replies_count > 0">
icon="reply"
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.reply')"
/>
</span>
<span
v-if="status.replies_count > 0"
class="action-counter"
>
{{ status.replies_count }} {{ status.replies_count }}
</span> </span>
</div> </div>
@@ -34,25 +26,14 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.ReplyButton { .ReplyButton {
display: flex; &.-interactive {
cursor: pointer;
> :first-child { &:hover,
padding: 10px; &.-active {
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
&:hover .svg-inline--fa,
&.-active .svg-inline--fa {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);
} }
} }
} }
</style> </style>
@@ -24,6 +24,11 @@ const RetweetButton = {
} }
}, },
computed: { computed: {
classes () {
return {
'-repeated': this.status.repeated
}
},
mergedConfig () { mergedConfig () {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
} }
@@ -1,38 +1,33 @@
<template> <template>
<div class="RetweetButton"> <div v-if="loggedIn">
<button <template v-if="visibility !== 'private' && visibility !== 'direct'">
v-if="visibility !== 'private' && visibility !== 'direct' && loggedIn"
class="button-unstyled interactive"
:class="status.repeated && '-repeated'"
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
>
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" :class="classes"
class="RetweetButton fa-scale-110 fa-old-padding -interactive"
icon="retweet" icon="retweet"
:spin="animated" :spin="animated"
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
/> />
</button> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
<span v-else-if="loggedIn"> </template>
<template v-else>
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" :class="classes"
class="RetweetButton fa-scale-110 fa-old-padding"
icon="lock" icon="lock"
:title="$t('timeline.no_retweet_hint')" :title="$t('timeline.no_retweet_hint')"
/> />
</span> </template>
<span v-else> </div>
<FAIcon <div v-else-if="!loggedIn">
class="fa-scale-110 fa-old-padding" <FAIcon
icon="retweet" :class="classes"
:title="$t('tool_tip.repeat')" class="fa-scale-110 fa-old-padding"
/> icon="retweet"
</span> :title="$t('tool_tip.repeat')"
<span />
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0" <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
class="no-event"
>
{{ status.repeat_num }}
</span>
</div> </div>
</template> </template>
@@ -42,28 +37,19 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.RetweetButton { .RetweetButton {
display: flex; &.-interactive {
cursor: pointer;
animation-duration: 0.6s;
> :first-child { &:hover {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
.svg-inline--fa {
animation-duration: 0.6s;
}
&:hover .svg-inline--fa,
&.-repeated .svg-inline--fa {
color: $fallback--cGreen; color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen); color: var(--cGreen, $fallback--cGreen);
} }
} }
&.-repeated {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
} }
</style> </style>
@@ -3,58 +3,54 @@
v-if="!showNothing" v-if="!showNothing"
class="ScopeSelector" class="ScopeSelector"
> >
<button <span
v-if="showDirect" v-if="showDirect"
class="button-unstyled scope" class="scope"
:class="css.direct" :class="css.direct"
:title="$t('post_status.scope.direct')" :title="$t('post_status.scope.direct')"
type="button"
@click="changeVis('direct')" @click="changeVis('direct')"
> >
<FAIcon <FAIcon
icon="envelope" icon="envelope"
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
/> />
</button> </span>
<button <span
v-if="showPrivate" v-if="showPrivate"
class="button-unstyled scope" class="scope"
:class="css.private" :class="css.private"
:title="$t('post_status.scope.private')" :title="$t('post_status.scope.private')"
type="button"
@click="changeVis('private')" @click="changeVis('private')"
> >
<FAIcon <FAIcon
icon="lock" icon="lock"
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
/> />
</button> </span>
<button <span
v-if="showUnlisted" v-if="showUnlisted"
class="button-unstyled scope" class="scope"
:class="css.unlisted" :class="css.unlisted"
:title="$t('post_status.scope.unlisted')" :title="$t('post_status.scope.unlisted')"
type="button"
@click="changeVis('unlisted')" @click="changeVis('unlisted')"
> >
<FAIcon <FAIcon
icon="lock-open" icon="lock-open"
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
/> />
</button> </span>
<button <span
v-if="showPublic" v-if="showPublic"
class="button-unstyled scope" class="scope"
:class="css.public" :class="css.public"
:title="$t('post_status.scope.public')" :title="$t('post_status.scope.public')"
type="button"
@click="changeVis('public')" @click="changeVis('public')"
> >
<FAIcon <FAIcon
icon="globe" icon="globe"
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
/> />
</button> </span>
</div> </div>
</template> </template>
+1 -2
View File
@@ -14,8 +14,7 @@
@keyup.enter="newQuery(searchTerm)" @keyup.enter="newQuery(searchTerm)"
> >
<button <button
class="btn button-default search-button" class="btn search-button"
type="submit"
@click="newQuery(searchTerm)" @click="newQuery(searchTerm)"
> >
<FAIcon icon="search" /> <FAIcon icon="search" />
+13 -22
View File
@@ -3,19 +3,17 @@
class="SearchBar" class="SearchBar"
:class="{ '-expanded': !hidden }" :class="{ '-expanded': !hidden }"
> >
<button <a
v-if="hidden" v-if="hidden"
class="button-unstyled nav-icon" href="#"
class="nav-icon"
:title="$t('nav.search')" :title="$t('nav.search')"
type="button" ><FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="search"
@click.prevent.stop="toggleHidden" @click.prevent.stop="toggleHidden"
> /></a>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="search"
/>
</button>
<template v-else> <template v-else>
<input <input
id="search-bar-input" id="search-bar-input"
@@ -27,8 +25,7 @@
@keyup.enter="find(searchTerm)" @keyup.enter="find(searchTerm)"
> >
<button <button
class="button-default search-button" class="btn search-button"
type="submit"
@click="find(searchTerm)" @click="find(searchTerm)"
> >
<FAIcon <FAIcon
@@ -36,17 +33,14 @@
icon="search" icon="search"
/> />
</button> </button>
<button <span>
class="button-unstyled cancel-search"
type="button"
@click.prevent.stop="toggleHidden"
>
<FAIcon <FAIcon
fixed-width fixed-width
icon="times" icon="times"
class="cancel-icon fa-scale-110 fa-old-padding" class="cancel-icon fa-scale-110 fa-old-padding"
@click.prevent.stop="toggleHidden"
/> />
</button> </span>
</template> </template>
</div> </div>
</template> </template>
@@ -75,11 +69,8 @@
flex: 1 0 auto; flex: 1 0 auto;
} }
.cancel-search {
height: 50px;
}
.cancel-icon { .cancel-icon {
cursor: pointer;
color: $fallback--text; color: $fallback--text;
color: var(--btnTopBarText, $fallback--text); color: var(--btnTopBarText, $fallback--text);
} }
-21
View File
@@ -1,21 +0,0 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown
)
export default {
model: {
prop: 'value',
event: 'change'
},
props: [
'value',
'disabled',
'unstyled',
'kind'
]
}
-62
View File
@@ -1,62 +0,0 @@
<template>
<label
class="Select input"
:class="{ disabled, unstyled }"
>
<select
:disabled="disabled"
:value="value"
@change="$emit('change', $event.target.value)"
>
<slot />
</select>
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</template>
<script src="./select.js"> </script>
<style lang="scss">
@import '../../_variables.scss';
.Select {
padding: 0;
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;
}
.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;
}
}
</style>
@@ -24,7 +24,10 @@
:items="items" :items="items"
:get-key="getKey" :get-key="getKey"
> >
<template v-slot:item="{item}"> <template
slot="item"
slot-scope="{item}"
>
<div <div
class="selectable-list-item-inner" class="selectable-list-item-inner"
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
@@ -41,7 +44,7 @@
/> />
</div> </div>
</template> </template>
<template v-slot:empty> <template slot="empty">
<slot name="empty" /> <slot name="empty" />
</template> </template>
</List> </List>
@@ -1,38 +0,0 @@
import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ModifiedIndicator from './modified_indicator.vue'
export default {
components: {
Checkbox,
ModifiedIndicator
},
props: [
'path',
'disabled'
],
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isChanged () {
return this.state !== this.defaultState
}
},
methods: {
update (e) {
set(this.$parent, this.path, e)
}
}
}
@@ -1,21 +0,0 @@
<template>
<label
class="BooleanSetting"
>
<Checkbox
:checked="state"
:disabled="disabled"
@change="update"
>
<span
v-if="!!$slots.default"
class="label"
>
<slot />
</span>
<ModifiedIndicator :changed="isChanged" />
</Checkbox>
</label>
</template>
<script src="./boolean_setting.js"></script>
@@ -1,39 +0,0 @@
import { get, set } from 'lodash'
import Select from 'src/components/select/select.vue'
import ModifiedIndicator from './modified_indicator.vue'
export default {
components: {
Select,
ModifiedIndicator
},
props: [
'path',
'disabled',
'options'
],
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isChanged () {
return this.state !== this.defaultState
}
},
methods: {
update (e) {
set(this.$parent, this.path, e)
}
}
}

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