Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into simplePolicy_reasons_for_instance_specific_policies

This commit is contained in:
Ilja 2021-03-20 08:56:21 +01:00
commit ab620e2cff
234 changed files with 13106 additions and 4399 deletions

1
.mailmap Normal file
View file

@ -0,0 +1 @@
rinpatch <rin@patch.cx> <rinpatch@sdf.org>

View file

@ -5,17 +5,116 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- New option to optimize timeline rendering to make the site more responsive (enabled by default) - Added a quick settings to timeline header for easier access
- Added option to mark posts as sensitive by default
- Added quick filters for notifications
## [Unreleased patch]
## [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
### Added
- New option to optimize timeline rendering to make the site more responsive (enabled by default)
- New instance option `logoLeft` to move logo to the left side in desktop nav bar
- Import/export a muted users
- 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 a small red badge to the favicon when there's unread notifications
- Added the NSFW alert to link previews
### Fixed
- Fixed clicking NSFW hider through status popover
- Fixed chat-view back button being hard to click
- Fixed fresh chat notifications being cleared immediately while leaving the chat view and not having time to actually see the messages
- Fixed multiple regressions in CSS styles
- Fixed multiple issues with input fields when using CJK font as default
- Fixed search field in navbar infringing into logo in some cases
- Fixed not being able to load the chat history in vertical screens when the message list doesn't take the full height of the scrollable container on the first fetch.
### Changed
- Clicking immediately when timeline shifts is now blocked to prevent misclicks
- Icons changed from fontello (FontAwesome 4 + others) to FontAwesome 5 due to problems with fontello.
- Some icons changed for better accessibility (lock, globe)
- Logo is now clickable
- Changed default logo to SVG version
## [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
- Fixed clicking NSFW hider through status popover
### Added
- Import/export a muted users
- Proper handling of deletes when using websocket streaming
## [2.1.1] - 2020-09-08 ## [2.1.1] - 2020-09-08
### Changed ### Changed
@ -142,8 +241,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ability to change user's email - Ability to change user's email
- About page - About page
- Added remote user redirect - Added remote user redirect
- Bookmarks
### Changed ### Changed
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
### Fixed ### Fixed
- improved hotkey behavior on autocomplete popup - improved hotkey behavior on autocomplete popup

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 FontelloPlugin = require("fontello-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,15 +93,6 @@ module.exports = {
new ServiceWorkerWebpackPlugin({ new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '..', 'src/sw.js'), entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js' filename: 'sw-pleroma.js'
}),
new FontelloPlugin({
config: require('../static/fontello.json'),
host: 'https://fontello.com',
name: 'fontello',
output: {
css: 'static/[name].' + now + '.css', // [hash] is not supported. Use the current timestamp instead for versioning.
font: 'static/font/[name].' + now + '.[ext]'
}
}) })
] ]
} }

View file

@ -3,6 +3,11 @@ 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) {

View file

@ -3,7 +3,6 @@
<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>

View file

@ -18,6 +18,10 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"@chenfengyuan/vue-qrcode": "^1.0.0", "@chenfengyuan/vue-qrcode": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^2.0.0",
"body-scroll-lock": "^2.6.4", "body-scroll-lock": "^2.6.4",
"chromatism": "^3.0.0", "chromatism": "^3.0.0",
"cropperjs": "^1.4.3", "cropperjs": "^1.4.3",
@ -27,9 +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",
"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",
@ -51,7 +55,7 @@
"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": "^2.21.2", "chromedriver": "^87.0.1",
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"cross-spawn": "^4.0.2", "cross-spawn": "^4.0.2",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
@ -68,7 +72,6 @@
"eventsource-polyfill": "^0.9.6", "eventsource-polyfill": "^0.9.6",
"express": "^4.13.3", "express": "^4.13.3",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"fontello-webpack-plugin": "https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186",
"function-bind": "^1.0.2", "function-bind": "^1.0.2",
"html-webpack-plugin": "^3.0.0", "html-webpack-plugin": "^3.0.0",
"http-proxy-middleware": "^0.17.2", "http-proxy-middleware": "^0.17.2",
@ -99,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.7.4", "shelljs": "^0.8.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",

View file

@ -1,7 +1,6 @@
import UserPanel from './components/user_panel/user_panel.vue' import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue' import Notifications from './components/notifications/notifications.vue'
import SearchBar from './components/search_bar/search_bar.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'
@ -11,10 +10,12 @@ 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'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue'
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
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',
@ -22,7 +23,6 @@ export default {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications, Notifications,
SearchBar,
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -31,21 +31,14 @@ export default {
SideDrawer, SideDrawer,
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
DesktopNav,
SettingsModal, SettingsModal,
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
GlobalNoticeList GlobalNoticeList
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline'
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
}), }),
created () { created () {
// Load the locale from the storage // Load the locale from the storage
@ -58,44 +51,21 @@ export default {
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
background () { userBackground () { return this.currentUser.background_image },
return this.currentUser.background_image || this.$store.state.instance.background instanceBackground () {
return this.mergedConfig.hideInstanceWallpaper
? null
: this.$store.state.instance.background
}, },
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, background () { return this.userBackground || this.instanceBackground },
logoStyle () {
return {
'visibility': this.enableMask ? 'hidden' : 'visible'
}
},
logoMaskStyle () {
return this.enableMask ? {
'mask-image': `url(${this.$store.state.instance.logo})`
} : {
'background-color': this.enableMask ? '' : 'transparent'
}
},
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
},
logo () { return this.$store.state.instance.logo },
bgStyle () { bgStyle () {
return { if (this.background) {
'background-image': `url(${this.background})` return {
'--body-background-image': `url(${this.background})`
}
} }
}, },
bgAppStyle () {
return {
'--body-background-image': `url(${this.background})`
}
},
sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' }, chat () { return this.$store.state.chat.channel.state === 'joined' },
hideSitename () { return this.$store.state.instance.hideSitename },
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 &&
@ -109,22 +79,10 @@ export default {
return { return {
'order': this.$store.state.instance.sidebarRight ? 99 : 0 'order': this.$store.state.instance.sidebarRight ? 99 : 0
} }
} },
...mapGetters(['mergedConfig'])
}, },
methods: { methods: {
scrollToTop () {
window.scrollTo(0, 0)
},
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
},
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
updateMobileState () { updateMobileState () {
const mobileLayout = windowWidth() <= 800 const mobileLayout = windowWidth() <= 800
const layoutHeight = windowHeight() const layoutHeight = windowHeight()

View file

@ -14,7 +14,9 @@
right: -20px; right: -20px;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 0 50%; background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50% 50px;
} }
i[class^='icon-'] { i[class^='icon-'] {
@ -33,6 +35,7 @@ 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));
@ -69,7 +72,7 @@ a {
color: var(--link, $fallback--link); color: var(--link, $fallback--link);
} }
button { .button-default {
user-select: none; user-select: none;
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
@ -85,7 +88,8 @@ button {
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 {
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
} }
@ -106,6 +110,8 @@ button {
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg); background-color: var(--btnPressed, $fallback--fg);
svg,
i { i {
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
@ -118,6 +124,8 @@ button {
color: var(--btnDisabledText, $fallback--text); color: var(--btnDisabledText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg); background-color: var(--btnDisabled, $fallback--fg);
svg,
i { i {
color: $fallback--text; color: $fallback--text;
color: var(--btnDisabledText, $fallback--text); color: var(--btnDisabledText, $fallback--text);
@ -131,6 +139,8 @@ button {
background-color: var(--btnToggled, $fallback--fg); background-color: var(--btnToggled, $fallback--fg);
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,
i { i {
color: $fallback--text; color: $fallback--text;
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
@ -146,6 +156,37 @@ button {
} }
} }
.button-unstyled {
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, .select, .input { input, textarea, .select, .input {
&.unstyled { &.unstyled {
@ -185,7 +226,7 @@ input, textarea, .select, .input {
opacity: 0.5; opacity: 0.5;
} }
.icon-down-open { .select-down-icon {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -300,6 +341,10 @@ input, textarea, .select, .input {
box-sizing: border-box; box-sizing: border-box;
} }
} }
&.resize-height {
resize: vertical;
}
} }
option { option {
@ -318,7 +363,7 @@ option {
} }
} }
i[class*=icon-] { i[class*=icon-], .svg-inline--fa {
color: $fallback--icon; color: $fallback--icon;
color: var(--icon, $fallback--icon); color: var(--icon, $fallback--icon);
} }
@ -356,117 +401,10 @@ i[class*=icon-] {
padding: 0 10px 0 10px; padding: 0 10px 0 10px;
} }
.item {
flex: 1;
line-height: 50px;
height: 50px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
.nav-icon {
margin-left: 0.4em;
}
&.right {
justify-content: flex-end;
}
}
.auto-size { .auto-size {
flex: 1 flex: 1
} }
.nav-bar {
padding: 0;
width: 100%;
align-items: center;
position: fixed;
height: 50px;
box-sizing: border-box;
button {
&, i[class*=icon-] {
color: $fallback--text;
color: var(--btnTopBarText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedTopBar, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedTopBarText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledTopBarText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg)
}
}
.logo {
display: flex;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
align-items: stretch;
justify-content: center;
flex: 0 0 auto;
z-index: -1;
transition: opacity;
transition-timing-function: ease-out;
transition-duration: 100ms;
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--topBarText, $fallback--fg);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
img {
height: 100%;
object-fit: contain;
display: block;
flex: 0;
}
}
.inner-nav {
position: relative;
margin: auto;
box-sizing: border-box;
padding-left: 10px;
padding-right: 10px;
display: flex;
align-items: center;
flex-basis: 970px;
height: 50px;
a, a i {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
}
}
}
main-router { main-router {
flex: 1; flex: 1;
} }
@ -546,6 +484,7 @@ 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);
@ -557,11 +496,8 @@ main-router {
overflow-x: hidden; overflow-x: hidden;
} }
button { .button-default,
flex-shrink: 0; .alert {
}
button, .alert {
// height: 100%; // height: 100%;
line-height: 21px; line-height: 21px;
min-height: 0; min-height: 0;
@ -572,8 +508,11 @@ main-router {
align-self: stretch; align-self: stretch;
} }
button { .button-default {
&, i[class*=icon-] { flex-shrink: 0;
&,
i[class*=icon-] {
color: $fallback--text; color: $fallback--text;
color: var(--btnPanelText, $fallback--text); color: var(--btnPanelText, $fallback--text);
} }
@ -596,7 +535,8 @@ main-router {
} }
} }
a { a,
.-link {
color: $fallback--link; color: $fallback--link;
color: var(--panelLink, $fallback--link) color: var(--panelLink, $fallback--link)
} }
@ -611,15 +551,15 @@ main-router {
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);
.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);
} }
} }
@ -646,6 +586,7 @@ 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 {
@ -707,19 +648,24 @@ nav {
flex-grow: 0; flex-grow: 0;
} }
} }
.badge { .badge {
box-sizing: border-box;
display: inline-block; display: inline-block;
border-radius: 99px; border-radius: 99px;
min-width: 22px; max-width: 10em;
max-width: 22px; min-width: 1.7em;
min-height: 22px; height: 1.3em;
max-height: 22px; padding: 0.15em 0.15em;
font-size: 15px;
line-height: 22px;
text-align: center;
vertical-align: middle; vertical-align: middle;
font-weight: normal;
font-style: normal;
font-size: 0.9em;
line-height: 1;
text-align: center;
white-space: nowrap; white-space: nowrap;
padding: 0; overflow: hidden;
text-overflow: ellipsis;
&.badge-notification { &.badge-notification {
background-color: $fallback--cRed; background-color: $fallback--cRed;
@ -760,6 +706,15 @@ 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 {
@ -776,16 +731,6 @@ nav {
} }
} }
@media all and (min-width: 800px) {
.logo {
opacity: 1 !important;
}
}
.item.right {
text-align: right;
}
.visibility-notice { .visibility-notice {
padding: .5em; padding: .5em;
border: 1px solid $fallback--faint; border: 1px solid $fallback--faint;
@ -807,8 +752,16 @@ nav {
} }
} }
.button-icon { .fa-scale-110 {
font-size: 1.2em; &.svg-inline--fa {
font-size: 1.1em;
}
}
.fa-old-padding {
&.svg-inline--fa {
padding: 0 0.3em;
}
} }
@keyframes shakeError { @keyframes shakeError {
@ -898,7 +851,7 @@ nav {
} }
} }
.btn.btn-default { .btn.button-default {
min-height: 28px; min-height: 28px;
} }
@ -930,27 +883,16 @@ nav {
background-color: var(--panel, $fallback--fg); background-color: var(--panel, $fallback--fg);
} }
.unread-chat-count {
font-size: 0.9em;
font-weight: bolder;
font-style: normal;
position: absolute;
right: 0.6rem;
padding: 0 0.3em;
min-width: 1.3rem;
min-height: 1.3rem;
max-height: 1.3rem;
line-height: 1.3rem;
max-width: 10em;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-layout { .chat-layout {
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
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) {

View file

@ -1,82 +1,14 @@
<template> <template>
<div <div
id="app" id="app"
:style="bgAppStyle" :style="bgStyle"
> >
<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" />
<nav <DesktopNav v-else />
v-else
id="nav"
class="nav-bar container"
@click="scrollToTop()"
>
<div class="inner-nav">
<div
class="logo"
:style="logoBgStyle"
>
<div
class="mask"
:style="logoMaskStyle"
/>
<img
:src="logo"
:style="logoStyle"
>
</div>
<div class="item">
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
</router-link>
</div>
<div class="item right">
<search-bar
v-if="currentUser || !privateMode"
class="nav-icon mobile-hidden"
@toggled="onSearchBarToggled"
@click.stop.native
/>
<a
href="#"
class="mobile-hidden"
@click.stop="openSettingsModal"
>
<i
class="button-icon icon-cog nav-icon"
:title="$t('nav.preferences')"
/>
</a>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="mobile-hidden"
target="_blank"
><i
class="button-icon icon-gauge nav-icon"
:title="$t('nav.administration')"
/></a>
<a
v-if="currentUser"
href="#"
class="mobile-hidden"
@click.prevent="logout"
><i
class="button-icon icon-logout nav-icon"
:title="$t('login.logout')"
/></a>
</div>
</div>
</nav>
<div class="app-bg-wrapper app-container-wrapper" /> <div class="app-bg-wrapper app-container-wrapper" />
<div <div
id="content" id="content"

View file

@ -7,6 +7,7 @@ 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
@ -50,6 +51,7 @@ 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 })
@ -130,6 +132,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
? 0 ? 0
: config.logoMargin : config.logoMargin
}) })
copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod) store.commit('authFlow/setInitialStrategy', config.loginMethod)
copyInstanceOption('redirectRootNoLogin') copyInstanceOption('redirectRootNoLogin')
@ -325,6 +328,8 @@ 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 })

View file

@ -1,6 +1,14 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisV
)
const AccountActions = { const AccountActions = {
props: [ props: [
@ -27,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', this.user.id) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
}, },
openChat () { openChat () {
this.$router.push({ this.$router.push({

View file

@ -1,9 +1,10 @@
<template> <template>
<div class="account-actions"> <div class="AccountActions">
<Popover <Popover
trigger="click" trigger="click"
placement="bottom" placement="bottom"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding
> >
<div <div
slot="content" slot="content"
@ -13,14 +14,14 @@
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <button
v-if="relationship.showing_reblogs" v-if="relationship.showing_reblogs"
class="btn btn-default dropdown-item" class="btn button-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 btn-default dropdown-item" class="btn button-default dropdown-item"
@click="showRepeats" @click="showRepeats"
> >
{{ $t('user_card.show_repeats') }} {{ $t('user_card.show_repeats') }}
@ -32,27 +33,27 @@
</template> </template>
<button <button
v-if="relationship.blocking" v-if="relationship.blocking"
class="btn btn-default btn-block dropdown-item" class="btn button-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 btn-default btn-block dropdown-item" class="btn button-default btn-block dropdown-item"
@click="blockUser" @click="blockUser"
> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</button> </button>
<button <button
class="btn btn-default btn-block dropdown-item" class="btn button-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 btn-default btn-block dropdown-item" class="btn button-default btn-block dropdown-item"
@click="openChat" @click="openChat"
> >
{{ $t('user_card.message') }} {{ $t('user_card.message') }}
@ -61,9 +62,12 @@
</div> </div>
<div <div
slot="trigger" slot="trigger"
class="btn btn-default ellipsis-button" class="ellipsis-button"
> >
<i class="icon-ellipsis trigger-button" /> <FAIcon
class="icon"
icon="ellipsis-v"
/>
</div> </div>
</Popover> </Popover>
</div> </div>
@ -73,22 +77,22 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.account-actions { .AccountActions {
margin: 0 .8em; button.dropdown-item {
} margin-left: 0;
}
.account-actions button.dropdown-item { .ellipsis-button {
margin-left: 0; cursor: pointer;
} width: 2.5em;
margin: -0.5em 0;
padding: 0.5em 0;
text-align: center;
.account-actions .trigger-button { &:not(:hover) .icon {
color: $fallback--lightText; color: $fallback--lightText;
color: var(--lightText, $fallback--lightText); color: var(--lightText, $fallback--lightText);
opacity: .8; }
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
} }
} }
</style> </style>

View file

@ -8,7 +8,7 @@
{{ $t('general.error_retry') }} {{ $t('general.error_retry') }}
</p> </p>
<button <button
class="btn" class="btn button-default"
@click="retry" @click="retry"
> >
{{ $t('general.retry') }} {{ $t('general.retry') }}

View file

@ -3,6 +3,24 @@ 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'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFile,
faMusic,
faImage,
faVideo,
faPlayCircle,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faVideo,
faPlayCircle,
faTimes
)
const Attachment = { const Attachment = {
props: [ props: [
@ -39,10 +57,10 @@ const Attachment = {
return this.attachment.description return this.attachment.description
}, },
placeholderIconClass () { placeholderIconClass () {
if (this.type === 'image') return 'icon-picture' if (this.type === 'image') return 'image'
if (this.type === 'video') return 'icon-video' if (this.type === 'video') return 'video'
if (this.type === 'audio') return 'icon-music' if (this.type === 'audio') return 'music'
return 'icon-doc' return 'file'
}, },
referrerpolicy () { referrerpolicy () {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'

View file

@ -12,7 +12,7 @@
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" :title="attachment.description"
> >
<span :class="placeholderIconClass" /> <FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a> </a>
</div> </div>
@ -36,20 +36,19 @@
:src="nsfwImage" :src="nsfwImage"
:class="{'small': isSmall}" :class="{'small': isSmall}"
> >
<i <FAIcon
v-if="type === 'video'" v-if="type === 'video'"
class="play-icon icon-play-circled" class="play-icon"
icon="play-circle"
/> />
</a> </a>
<div <button
v-if="nsfw && hideNsfwLocal && !hidden" v-if="nsfw && hideNsfwLocal && !hidden"
class="hider" class="button-unstyled hider"
@click.prevent="toggleHidden"
> >
<a <FAIcon icon="times" />
href="#" </button>
@click.prevent="toggleHidden"
>Hide</a>
</div>
<a <a
v-if="type === 'image' && (!hidden || preloadImage)" v-if="type === 'image' && (!hidden || preloadImage)"
@ -83,9 +82,10 @@
@play="$emit('play')" @play="$emit('play')"
@pause="$emit('pause')" @pause="$emit('pause')"
/> />
<i <FAIcon
v-if="!allowPlay" v-if="!allowPlay"
class="play-icon icon-play-circled" class="play-icon"
icon="play-circle"
/> />
</a> </a>
@ -142,6 +142,10 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 100%; max-width: 100%;
svg {
color: inherit;
}
} }
.nsfw-placeholder { .nsfw-placeholder {
@ -228,15 +232,23 @@
.hider { .hider {
position: absolute; position: absolute;
right: 0; right: 0;
white-space: nowrap;
margin: 10px; margin: 10px;
padding: 5px; padding: 0;
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 {

View file

@ -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 }} @{{ user.screen_name_ui }}
</router-link> </router-link>
</div> </div>
<slot /> <slot />

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 btn-default" class="btn button-default"
:disabled="progress" :disabled="progress"
@click="unblockUser" @click="unblockUser"
> >
@ -16,7 +16,7 @@
</button> </button>
<button <button
v-else v-else
class="btn btn-default" class="btn button-default"
:disabled="progress" :disabled="progress"
@click="blockUser" @click="blockUser"
> >

View file

@ -6,11 +6,24 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue' import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js' import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
library.add(
faChevronDown,
faChevronLeft
)
const BOTTOMED_OUT_OFFSET = 10 const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
const SAFE_RESIZE_TIME_OFFSET = 100 const SAFE_RESIZE_TIME_OFFSET = 100
const MARK_AS_READ_DELAY = 1500
const MAX_RETRIES = 10
const Chat = { const Chat = {
components: { components: {
@ -24,7 +37,8 @@ const Chat = {
hoveredMessageChainId: undefined, hoveredMessageChainId: undefined,
lastScrollPosition: {}, lastScrollPosition: {},
scrollableContainerHeight: '100%', scrollableContainerHeight: '100%',
errorLoadingChat: false errorLoadingChat: false,
messageRetriers: {}
} }
}, },
created () { created () {
@ -59,7 +73,7 @@ const Chat = {
}, },
formPlaceholder () { formPlaceholder () {
if (this.recipient) { if (this.recipient) {
return this.$t('chats.message_user', { nickname: this.recipient.screen_name }) return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
} else { } else {
return '' return ''
} }
@ -94,7 +108,7 @@ const Chat = {
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET) const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
this.$nextTick(() => { this.$nextTick(() => {
if (bottomedOutBeforeUpdate) { if (bottomedOutBeforeUpdate) {
this.scrollDown({ forceRead: !document.hidden }) this.scrollDown()
} }
}) })
}, },
@ -200,7 +214,7 @@ const Chat = {
this.$nextTick(() => { this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior }) scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
}) })
if (forceRead || this.newMessageCount > 0) { if (forceRead) {
this.readChat() this.readChat()
} }
}, },
@ -208,7 +222,10 @@ const Chat = {
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
if (document.hidden) { return } if (document.hidden) { return }
const lastReadId = this.currentChatMessageService.maxId const lastReadId = this.currentChatMessageService.maxId
this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) this.$store.dispatch('readChat', {
id: this.currentChat.id,
lastReadId
})
}, },
bottomedOut (offset) { bottomedOut (offset) {
return isBottomedOut(this.$refs.scrollable, offset) return isBottomedOut(this.$refs.scrollable, offset)
@ -217,6 +234,13 @@ 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 }
@ -224,13 +248,20 @@ 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) {
this.readChat() // 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
// get to see get marked as read.
window.setTimeout(() => {
// Don't mark as read if the element doesn't exist, user has left chat view
if (this.$el) this.readChat()
}, MARK_AS_READ_DELAY)
} }
} else { } else {
this.jumpToBottomButtonVisible = true this.jumpToBottomButtonVisible = true
} }
}, 100), }, 200),
handleScrollUp (positionBeforeLoading) { handleScrollUp (positionBeforeLoading) {
const positionAfterLoading = getScrollPosition(this.$refs.scrollable) const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
this.$refs.scrollable.scrollTo({ this.$refs.scrollable.scrollTo({
@ -264,6 +295,14 @@ const Chat = {
if (isFirstFetch) { if (isFirstFetch) {
this.updateScrollableContainerHeight() this.updateScrollableContainerHeight()
} }
// In vertical screens, the first batch of fetched messages may not always take the
// full height of the scrollable container.
// If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
this.fetchChat({ maxId: this.currentChatMessageService.minId })
}
}) })
}) })
}) })
@ -292,42 +331,74 @@ const Chat = {
}) })
this.fetchChat({ isFirstFetch: true }) this.fetchChat({ isFirstFetch: true })
}, },
sendMessage ({ status, media }) { handleAttachmentPosting () {
this.$nextTick(() => {
this.handleResize()
// When the posting form size changes because of a media attachment, we need an extra resize
// to account for the potential delay in the DOM update.
setTimeout(() => {
this.updateScrollableContainerHeight()
}, SAFE_RESIZE_TIME_OFFSET)
this.scrollDown({ forceRead: true })
})
},
sendMessage ({ status, media, idempotencyKey }) {
const params = { const params = {
id: this.currentChat.id, id: this.currentChat.id,
content: status content: status,
idempotencyKey
} }
if (media[0]) { if (media[0]) {
params.mediaId = media[0].id params.mediaId = media[0].id
} }
return this.backendInteractor.sendChatMessage(params) const fakeMessage = buildFakeMessage({
attachments: media,
chatId: this.currentChat.id,
content: status,
userId: this.currentUser.id,
idempotencyKey
})
this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id,
messages: [fakeMessage]
}).then(() => {
this.handleAttachmentPosting()
})
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
},
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
if (retriesLeft <= 0) return
this.backendInteractor.sendChatMessage(params)
.then(data => { .then(data => {
this.$store.dispatch('addChatMessages', { this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id, chatId: this.currentChat.id,
messages: [data], updateMaxId: false,
updateMaxId: false messages: [{ ...data, fakeId: fakeMessage.id }]
}).then(() => {
this.$nextTick(() => {
this.handleResize()
// When the posting form size changes because of a media attachment, we need an extra resize
// to account for the potential delay in the DOM update.
setTimeout(() => {
this.updateScrollableContainerHeight()
}, SAFE_RESIZE_TIME_OFFSET)
this.scrollDown({ forceRead: true })
})
}) })
return data return data
}) })
.catch(error => { .catch(error => {
console.error('Error sending message', error) console.error('Error sending message', error)
return { this.$store.dispatch('handleMessageError', {
error: this.$t('chats.error_sending_message') chatId: this.currentChat.id,
fakeId: fakeMessage.id,
isRetry: retriesLeft !== MAX_RETRIES
})
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
} }
return {}
}) })
return Promise.resolve(fakeMessage)
}, },
goBack () { goBack () {
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })

View file

@ -25,7 +25,7 @@
min-height: 100%; min-height: 100%;
margin: 0 0 0 0; margin: 0 0 0 0;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ; border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
&::after { &::after {
border-radius: 0; border-radius: 0;
@ -58,12 +58,10 @@
.go-back-button { .go-back-button {
cursor: pointer; cursor: pointer;
margin-right: 1.4em; width: 28px;
text-align: center;
i { padding: 0.6em;
display: flex; margin: -0.6em 0.6em -0.6em -0.6em;
align-items: center;
}
} }
.jump-to-bottom-button { .jump-to-bottom-button {
@ -78,7 +76,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
z-index: 10; z-index: 10;
transition: 0.35s all; transition: 0.35s all;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1); transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
@ -100,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; padding: 0.1em;
border-radius: 50px;
position: absolute;
} }
.chat-loading-error { .chat-loading-error {
@ -140,11 +138,21 @@
} }
.chat-view-heading { .chat-view-heading {
box-sizing: border-box;
position: static; position: static;
z-index: 9999; z-index: 9999;
top: 0; top: 0;
margin-top: 0; margin-top: 0;
border-radius: 0; border-radius: 0;
/* This practically overlays the panel heading color over panel background
* color. This is needed because we allow transparent panel background and
* it doesn't work well in this "disjointed panel header" case
*/
background:
linear-gradient(to top, var(--panel), var(--panel)),
linear-gradient(to top, var(--bg), var(--bg));
height: 50px;
} }
.scrollable-message-list { .scrollable-message-list {

View file

@ -14,7 +14,10 @@
class="go-back-button" class="go-back-button"
@click="goBack" @click="goBack"
> >
<i class="button-icon icon-left-open" /> <FAIcon
size="lg"
icon="chevron-left"
/>
</a> </a>
<div class="title text-center"> <div class="title text-center">
<ChatTitle <ChatTitle
@ -58,14 +61,15 @@
:class="{ 'visible': jumpToBottomButtonVisible }" :class="{ 'visible': jumpToBottomButtonVisible }"
@click="scrollDown({ behavior: 'smooth' })" @click="scrollDown({ behavior: 'smooth' })"
> >
<i class="icon-down-open"> <span>
<FAIcon icon="chevron-down" />
<div <div
v-if="newMessageCount" v-if="newMessageCount"
class="badge badge-notification unread-chat-count unread-message-count" class="badge badge-notification unread-chat-count unread-message-count"
> >
{{ newMessageCount }} {{ newMessageCount }}
</div> </div>
</i> </span>
</div> </div>
<PostStatusForm <PostStatusForm
:disable-subject="true" :disable-subject="true"
@ -76,6 +80,7 @@
:disable-sensitivity-checkbox="true" :disable-sensitivity-checkbox="true"
:disable-submit="errorLoadingChat || !currentChat" :disable-submit="errorLoadingChat || !currentChat"
:disable-preview="true" :disable-preview="true"
:optimistic-posting="true"
:post-handler="sendMessage" :post-handler="sendMessage"
:submit-on-enter="!mobileLayout" :submit-on-enter="!mobileLayout"
:preserve-focus="!mobileLayout" :preserve-focus="!mobileLayout"

View file

@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => {
export const scrollableContainerHeight = (inner, header, footer) => { export const scrollableContainerHeight = (inner, header, footer) => {
return inner.offsetHeight - header.clientHeight - footer.clientHeight return inner.offsetHeight - header.clientHeight - footer.clientHeight
} }
// Returns whether or not the scrollbar is visible.
export const isScrollable = (el) => {
if (!el) return
return el.scrollHeight > el.clientHeight
}

View file

@ -10,7 +10,10 @@
<span class="title"> <span class="title">
{{ $t("chats.chats") }} {{ $t("chats.chats") }}
</span> </span>
<button @click="newChat"> <button
class="button-default"
@click="newChat"
>
{{ $t("chats.new") }} {{ $t("chats.new") }}
</button> </button>
</div> </div>

View file

@ -21,6 +21,12 @@
/> />
</span> </span>
<span class="heading-right" /> <span class="heading-right" />
<div class="time-wrapper">
<Timeago
:time="chat.updated_at"
:auto-update="60"
/>
</div>
</div> </div>
<div class="chat-preview"> <div class="chat-preview">
<StatusContent <StatusContent
@ -35,12 +41,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="time-wrapper">
<Timeago
:time="chat.updated_at"
:auto-update="60"
/>
</div>
</div> </div>
</template> </template>

View file

@ -7,6 +7,16 @@ import LinkPreview from '../link-preview/link-preview.vue'
import StatusContent from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import ChatMessageDate from '../chat_message_date/chat_message_date.vue' import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes,
faEllipsisH
)
const ChatMessage = { const ChatMessage = {
name: 'ChatMessage', name: 'ChatMessage',

View file

@ -24,16 +24,13 @@
} }
} }
.icon-ellipsis { .menu-icon {
cursor: pointer; cursor: pointer;
&:hover, .extra-button-popover.open & { &:hover, .extra-button-popover.open & {
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 {
@ -101,6 +98,19 @@
} }
} }
.pending {
.status-content.media-body, .created-at {
color: var(--faint);
}
}
.error {
.status-content.media-body, .created-at {
color: $fallback--cRed;
color: var(--badgeNotification, $fallback--cRed);
}
}
.incoming { .incoming {
a { a {
color: var(--chatMessageIncomingLink, $fallback--link); color: var(--chatMessageIncomingLink, $fallback--link);

View file

@ -32,7 +32,7 @@
> >
<div <div
class="media status" class="media status"
:class="{ 'without-attachment': !hasAttachment }" :class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
style="position: relative" style="position: relative"
@mouseenter="hovered = true" @mouseenter="hovered = true"
@mouseleave="hovered = false" @mouseleave="hovered = false"
@ -53,18 +53,19 @@
<div slot="content"> <div slot="content">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click="deleteMessage" @click="deleteMessage"
> >
<i class="icon-cancel" /> {{ $t("chats.delete") }} <FAIcon icon="times" /> {{ $t("chats.delete") }}
</button> </button>
</div> </div>
</div> </div>
<button <button
slot="trigger" slot="trigger"
class="button-default menu-icon"
:title="$t('chats.more')" :title="$t('chats.more')"
> >
<i class="icon-ellipsis" /> <FAIcon icon="ellipsis-h" />
</button> </button>
</Popover> </Popover>
</div> </div>

View file

@ -5,6 +5,8 @@
</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'],
@ -16,7 +18,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('en', { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
} }
} }
} }

View file

@ -1,6 +1,16 @@
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSearch,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add(
faSearch,
faChevronLeft
)
const chatNew = { const chatNew = {
components: { components: {

View file

@ -8,9 +8,7 @@
} }
} }
.icon-search { .search-icon {
font-size: 1.5em;
float: right;
margin-right: 0.3em; margin-right: 0.3em;
} }
@ -25,5 +23,9 @@
.go-back-button { .go-back-button {
cursor: pointer; cursor: pointer;
width: 28px;
text-align: center;
padding: 0.6em;
margin: -0.6em 0.6em -0.6em -0.6em;
} }
} }

View file

@ -11,12 +11,18 @@
class="go-back-button" class="go-back-button"
@click="goBack" @click="goBack"
> >
<i class="button-icon icon-left-open" /> <FAIcon
size="lg"
icon="chevron-left"
/>
</a> </a>
</div> </div>
<div class="input-wrap"> <div class="input-wrap">
<div class="input-search"> <div class="input-search">
<i class="button-icon icon-search" /> <FAIcon
class="search-icon fa-scale-110 fa-old-padding"
icon="search"
/>
</div> </div>
<input <input
ref="search" ref="search"

View file

@ -1,4 +1,14 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBullhorn,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faBullhorn,
faTimes
)
const chatPanel = { const chatPanel = {
props: [ 'floating' ], props: [ 'floating' ],
@ -25,6 +35,18 @@ const chatPanel = {
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
})
}
}
} }
} }

View file

@ -10,17 +10,15 @@
@click.stop.prevent="togglePanel" @click.stop.prevent="togglePanel"
> >
<div class="title"> <div class="title">
<span>{{ $t('shoutbox.title') }}</span> {{ $t('shoutbox.title') }}
<i <FAIcon
v-if="floating" v-if="floating"
class="icon-cancel" icon="times"
class="close-icon"
/> />
</div> </div>
</div> </div>
<div <div class="chat-window">
v-chat-scroll
class="chat-window"
>
<div <div
v-for="message in messages" v-for="message in messages"
:key="message.id" :key="message.id"
@ -63,7 +61,10 @@
@click.stop.prevent="togglePanel" @click.stop.prevent="togglePanel"
> >
<div class="title"> <div class="title">
<i class="icon-megaphone" /> <FAIcon
class="icon"
icon="bullhorn"
/>
{{ $t('shoutbox.title') }} {{ $t('shoutbox.title') }}
</div> </div>
</div> </div>
@ -87,9 +88,17 @@
.chat-panel { .chat-panel {
.chat-heading { .chat-heading {
cursor: pointer; cursor: pointer;
.icon-comment-empty {
.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;
} }
} }

View file

@ -12,7 +12,7 @@ export default Vue.component('chat-title', {
], ],
computed: { computed: {
title () { title () {
return this.user ? this.user.screen_name : '' return this.user ? this.user.screen_name_ui : ''
}, },
htmlTitle () { htmlTitle () {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''

View file

@ -8,13 +8,13 @@
class="rating" class="rating"
> >
<span v-if="contrast.aaa"> <span v-if="contrast.aaa">
<i class="icon-thumbs-up-alt" /> <FAIcon icon="thumbs-up" />
</span> </span>
<span v-if="!contrast.aaa && contrast.aa"> <span v-if="!contrast.aaa && contrast.aa">
<i class="icon-adjust" /> <FAIcon icon="adjust" />
</span> </span>
<span v-if="!contrast.aaa && !contrast.aa"> <span v-if="!contrast.aaa && !contrast.aa">
<i class="icon-attention" /> <FAIcon icon="exclamation-triangle" />
</span> </span>
</span> </span>
<span <span
@ -23,19 +23,32 @@
:title="hint_18pt" :title="hint_18pt"
> >
<span v-if="contrast.laaa"> <span v-if="contrast.laaa">
<i class="icon-thumbs-up-alt" /> <FAIcon icon="thumbs-up" />
</span> </span>
<span v-if="!contrast.laaa && contrast.laa"> <span v-if="!contrast.laaa && contrast.laa">
<i class="icon-adjust" /> <FAIcon icon="adjust" />
</span> </span>
<span v-if="!contrast.laaa && !contrast.laa"> <span v-if="!contrast.laaa && !contrast.laa">
<i class="icon-attention" /> <FAIcon icon="exclamation-triangle" />
</span> </span>
</span> </span>
</span> </span>
</template> </template>
<script> <script>
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAdjust,
faExclamationTriangle,
faThumbsUp
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAdjust,
faExclamationTriangle,
faThumbsUp
)
export default { export default {
props: { props: {
large: { large: {
@ -85,6 +98,7 @@ export default {
.rating { .rating {
display: inline-block; display: inline-block;
text-align: center; text-align: center;
margin-left: 0.5em;
} }
} }
</style> </style>

View file

@ -10,12 +10,13 @@
class="panel-heading conversation-heading" class="panel-heading conversation-heading"
> >
<span class="title"> {{ $t('timeline.conversation') }} </span> <span class="title"> {{ $t('timeline.conversation') }} </span>
<span v-if="collapsable"> <button
<a v-if="collapsable"
href="#" class="button-unstyled -link"
@click.prevent="toggleExpanded" @click.prevent="toggleExpanded"
>{{ $t('timeline.collapse') }}</a> >
</span> {{ $t('timeline.collapse') }}
</button>
</div> </div>
<status <status
v-for="status in conversation" v-for="status in conversation"
@ -49,7 +50,6 @@
.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,13 +57,6 @@
} }
&.-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;

View file

@ -0,0 +1,89 @@
import SearchBar from 'components/search_bar/search_bar.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSignInAlt,
faSignOutAlt,
faHome,
faComments,
faBell,
faUserPlus,
faBullhorn,
faSearch,
faTachometerAlt,
faCog,
faInfoCircle
} from '@fortawesome/free-solid-svg-icons'
library.add(
faSignInAlt,
faSignOutAlt,
faHome,
faComments,
faBell,
faUserPlus,
faBullhorn,
faSearch,
faTachometerAlt,
faCog,
faInfoCircle
)
export default {
components: {
SearchBar
},
data: () => ({
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
}),
computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
logoStyle () {
return {
'visibility': this.enableMask ? 'hidden' : 'visible'
}
},
logoMaskStyle () {
return this.enableMask ? {
'mask-image': `url(${this.$store.state.instance.logo})`
} : {
'background-color': this.enableMask ? '' : 'transparent'
}
},
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
},
logo () { return this.$store.state.instance.logo },
sitename () { return this.$store.state.instance.name },
hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private }
},
methods: {
scrollToTop () {
window.scrollTo(0, 0)
},
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
},
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
}
}
}

View file

@ -0,0 +1,117 @@
@import '../../_variables.scss';
.DesktopNav {
height: 50px;
width: 100%;
position: fixed;
a {
color: var(--topBarLink, $fallback--link);
}
.inner-nav {
display: grid;
grid-template-rows: 50px;
grid-template-columns: 2fr auto 2fr;
grid-template-areas: "sitename logo actions";
box-sizing: border-box;
padding: 0 1.2em;
margin: auto;
max-width: 980px;
}
&.-logoLeft {
grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo sitename actions";
}
.button-default {
&, svg {
color: $fallback--text;
color: var(--btnTopBarText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedTopBar, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedTopBarText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledTopBarText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg)
}
}
.logo {
grid-area: logo;
position: relative;
transition: opacity;
transition-timing-function: ease-out;
transition-duration: 100ms;
@media all and (min-width: 800px) {
opacity: 1 !important;
}
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--topBarText, $fallback--fg);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
img {
display: inline-block;
height: 50px;
}
}
.nav-icon {
margin-left: 0.2em;
width: 2em;
height: 100%;
text-align: center;
.svg-inline--fa {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
}
}
.sitename {
grid-area: sitename;
}
.actions {
grid-area: actions;
}
.item {
flex: 1;
line-height: 50px;
height: 50px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
&.right {
justify-content: flex-end;
text-align: right;
}
}
}

View file

@ -0,0 +1,81 @@
<template>
<nav
id="nav"
class="DesktopNav"
:class="{ '-logoLeft': logoLeft }"
@click="scrollToTop()"
>
<div class="inner-nav">
<div class="item sitename">
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
</router-link>
</div>
<router-link
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
>
<div
class="mask"
:style="logoMaskStyle"
/>
<img
:src="logo"
:style="logoStyle"
>
</router-link>
<div class="item right actions">
<search-bar
v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled"
@click.stop.native
/>
<button
class="button-unstyled nav-icon"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
:title="$t('nav.preferences')"
/>
</button>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="nav-icon"
target="_blank"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
:title="$t('nav.administration')"
/>
</a>
<button
v-if="currentUser"
class="button-unstyled nav-icon"
@click.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div>
</div>
</nav>
</template>
<script src="./desktop_nav.js"></script>
<style src="./desktop_nav.scss" lang="scss"></style>

View file

@ -6,7 +6,7 @@
<ProgressButton <ProgressButton
v-if="muted" v-if="muted"
:click="unmuteDomain" :click="unmuteDomain"
class="btn btn-default" class="btn button-default"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template slot="progress"> <template slot="progress">
@ -16,7 +16,7 @@
<ProgressButton <ProgressButton
v-else v-else
:click="muteDomain" :click="muteDomain"
class="btn btn-default" class="btn button-default"
> >
{{ $t('domain_mute_card.mute') }} {{ $t('domain_mute_card.mute') }}
<template slot="progress"> <template slot="progress">

View file

@ -3,6 +3,15 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import { take } from 'lodash' import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSmileBeam
} from '@fortawesome/free-regular-svg-icons'
library.add(
faSmileBeam
)
/** /**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
* without having to give up the comfort of <input/> and <textarea/> elements * without having to give up the comfort of <input/> and <textarea/> elements
@ -105,7 +114,8 @@ const EmojiInput = {
showPicker: false, showPicker: false,
temporarilyHideSuggestions: false, temporarilyHideSuggestions: false,
keepOpen: false, keepOpen: false,
disableClickOutside: false disableClickOutside: false,
suggestions: []
} }
}, },
components: { components: {
@ -115,21 +125,6 @@ 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 &&
@ -179,14 +174,38 @@ const EmojiInput = {
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
@ -202,6 +221,7 @@ const EmojiInput = {
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) {

View file

@ -6,13 +6,14 @@
> >
<slot /> <slot />
<template v-if="enableEmojiPicker"> <template v-if="enableEmojiPicker">
<div <button
v-if="!hideEmojiButton" v-if="!hideEmojiButton"
class="emoji-picker-icon" class="button-unstyled emoji-picker-icon"
type="button"
@click.prevent="togglePicker" @click.prevent="togglePicker"
> >
<i class="icon-smile" /> <FAIcon :icon="['far', 'smile-beam']" />
</div> </button>
<EmojiPicker <EmojiPicker
v-if="enableEmojiPicker" v-if="enableEmojiPicker"
ref="picker" ref="picker"
@ -37,7 +38,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: suggestion.highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span class="image"> <span class="image">

View file

@ -1,4 +1,3 @@
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:
@ -11,19 +10,19 @@ import { debounce } from 'lodash'
* doesn't support user linking you can just provide only emoji. * doesn't support user linking you can just provide only emoji.
*/ */
const debounceUserSearch = debounce((data, input) => { export default data => {
data.updateUsersList(input) const emojiCurry = suggestEmoji(data.emoji)
}, 500) const usersCurry = data.store && suggestUsers(data.store)
return input => {
export default data => input => { const firstChar = input[0]
const firstChar = input[0] if (firstChar === ':' && data.emoji) {
if (firstChar === ':' && data.emoji) { return emojiCurry(input)
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 => {
@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => {
}) })
} }
export const suggestUsers = data => input => { export const suggestUsers = ({ dispatch, state }) => {
const noPrefix = input.toLowerCase().substr(1) // Keep some persistent values in closure, most importantly for the
const users = data.users // custom debounce to work. Lodash debounce does not return a promise.
let suggestions = []
let previousQuery = ''
let timeout = null
let cancelUserSearch = null
const newUsers = users.filter( const userSearch = (query) => dispatch('searchUsers', { query })
user => const debounceUserSearch = (query) => {
user.screen_name.toLowerCase().startsWith(noPrefix) || cancelUserSearch && cancelUserSearch()
user.name.toLowerCase().startsWith(noPrefix) return new Promise((resolve, reject) => {
timeout = setTimeout(() => {
/* taking only 20 results so that sorting is a bit cheaper, we display userSearch(query).then(resolve).catch(reject)
* only 5 anyway. could be inaccurate, but we ideally we should query }, 300)
* backend anyway cancelUserSearch = () => {
*/ clearTimeout(timeout)
).slice(0, 20).sort((a, b) => { resolve([])
let aScore = 0 }
let bScore = 0 })
}
// Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 return async input => {
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 suggestions = []
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 previousQuery = noPrefix
// Fetch more and wait, don't fetch if there's the 2nd @ because
const diff = (bScore - aScore) * 10 // the backend user search can't deal with it.
// Reference semantics make it so that we get the updated data after
// Then sort alphabetically // the await.
const nameAlphabetically = a.name > b.name ? 1 : -1 if (!noPrefix.includes('@')) {
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 await debounceUserSearch(noPrefix)
}
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */ const newSuggestions = state.users.users.filter(
}).map(({ screen_name, name, profile_image_url_original }) => ({ user =>
displayText: screen_name, user.screen_name.toLowerCase().startsWith(noPrefix) ||
detailText: name, user.name.toLowerCase().startsWith(noPrefix)
imageUrl: profile_image_url_original, ).slice(0, 20).sort((a, b) => {
replacement: '@' + screen_name + ' ' let aScore = 0
})) let bScore = 0
// BE search users to get more comprehensive results // Matches on screen name (i.e. user@instance) makes a priority
if (data.updateUsersList) { aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
debounceUserSearch(data, noPrefix) bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// 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 */
} }

View file

@ -1,4 +1,16 @@
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
library.add(
faBoxOpen,
faStickyNote,
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row, // At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker // loading 3 rows, could be overkill for narrow picker
@ -177,13 +189,13 @@ const EmojiPicker = {
{ {
id: 'custom', id: 'custom',
text: this.$t('emoji.custom'), text: this.$t('emoji.custom'),
icon: 'icon-smile', icon: 'smile-beam',
emojis: customEmojis emojis: customEmojis
}, },
{ {
id: 'standard', id: 'standard',
text: this.$t('emoji.unicode'), text: this.$t('emoji.unicode'),
icon: 'icon-picture', icon: 'box-open',
emojis: filterByKeyword(standardEmojis, this.keyword) emojis: filterByKeyword(standardEmojis, this.keyword)
} }
] ]

View file

@ -82,7 +82,7 @@
&.active { &.active {
border-bottom: 4px solid; border-bottom: 4px solid;
i { svg {
color: $fallback--lightText; color: $fallback--lightText;
color: var(--lightText, $fallback--lightText); color: var(--lightText, $fallback--lightText);
} }

View file

@ -13,7 +13,10 @@
:title="group.text" :title="group.text"
@click.prevent="highlight(group.id)" @click.prevent="highlight(group.id)"
> >
<i :class="group.icon" /> <FAIcon
:icon="group.icon"
fixed-width
/>
</span> </span>
</span> </span>
<span <span
@ -26,7 +29,10 @@
:title="$t('emoji.stickers')" :title="$t('emoji.stickers')"
@click.prevent="toggleStickers" @click.prevent="toggleStickers"
> >
<i class="icon-star" /> <FAIcon
icon="sticky-note"
fixed-width
/>
</span> </span>
</span> </span>
</div> </div>

View file

@ -6,7 +6,7 @@
:users="accountsForEmoji[reaction.name]" :users="accountsForEmoji[reaction.name]"
> >
<button <button
class="emoji-reaction btn btn-default" class="emoji-reaction btn button-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()"

View file

@ -2,13 +2,13 @@
<div class="import-export-container"> <div class="import-export-container">
<slot name="before" /> <slot name="before" />
<button <button
class="btn" class="btn button-default"
@click="exportData" @click="exportData"
> >
{{ exportLabel }} {{ exportLabel }}
</button> </button>
<button <button
class="btn" class="btn button-default"
@click="importData" @click="importData"
> >
{{ importLabel }} {{ importLabel }}

View file

@ -1,3 +1,10 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
const Exporter = { const Exporter = {
props: { props: {
getContent: { getContent: {

View file

@ -1,12 +1,17 @@
<template> <template>
<div class="exporter"> <div class="exporter">
<div v-if="processing"> <div v-if="processing">
<i class="icon-spin4 animate-spin exporter-processing" /> <FAIcon
icon="circle-notch"
size="lg"
spin
/>
<span>{{ processingMessage }}</span> <span>{{ processingMessage }}</span>
</div> </div>
<button <button
v-else v-else
class="btn btn-default" class="btn button-default"
@click="process" @click="process"
> >
{{ exportButtonLabel }} {{ exportButtonLabel }}
@ -19,7 +24,6 @@
<style lang="scss"> <style lang="scss">
.exporter { .exporter {
&-processing { &-processing {
font-size: 1.5em;
margin: 0.25em; margin: 0.25em;
} }
} }

View file

@ -1,4 +1,28 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as faBookmarkReg,
faFlag
} from '@fortawesome/free-regular-svg-icons'
library.add(
faEllipsisH,
faBookmark,
faBookmarkReg,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faFlag
)
const ExtraButtons = { const ExtraButtons = {
props: [ 'status' ], props: [ 'status' ],
@ -44,6 +68,9 @@ 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: {

View file

@ -1,9 +1,11 @@
<template> <template>
<Popover <Popover
class="ExtraButtons"
trigger="click" trigger="click"
placement="top" placement="top"
class="extra-button-popover" :offset="{ y: 5 }"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding
> >
<div <div
slot="content" slot="content"
@ -12,71 +14,122 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="muteConversation" @click.prevent="muteConversation"
> >
<i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span> <FAIcon
fixed-width
icon="eye-slash"
/><span>{{ $t("status.mute_conversation") }}</span>
</button> </button>
<button <button
v-if="canMute && status.thread_muted" v-if="canMute && status.thread_muted"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unmuteConversation" @click.prevent="unmuteConversation"
> >
<i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span> <FAIcon
fixed-width
icon="eye-slash"
/><span>{{ $t("status.unmute_conversation") }}</span>
</button> </button>
<button <button
v-if="!status.pinned && canPin" v-if="!status.pinned && canPin"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="pinStatus" @click.prevent="pinStatus"
@click="close" @click="close"
> >
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span> <FAIcon
fixed-width
icon="thumbtack"
/><span>{{ $t("status.pin") }}</span>
</button> </button>
<button <button
v-if="status.pinned && canPin" v-if="status.pinned && canPin"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus" @click.prevent="unpinStatus"
@click="close" @click="close"
> >
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span> <FAIcon
fixed-width
icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span>
</button> </button>
<button <button
v-if="!status.bookmarked" v-if="!status.bookmarked"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="bookmarkStatus" @click.prevent="bookmarkStatus"
@click="close" @click="close"
> >
<i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span> <FAIcon
fixed-width
:icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span>
</button> </button>
<button <button
v-if="status.bookmarked" v-if="status.bookmarked"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unbookmarkStatus" @click.prevent="unbookmarkStatus"
@click="close" @click="close"
> >
<i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span> <FAIcon
fixed-width
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
</button> </button>
<button <button
v-if="canDelete" v-if="canDelete"
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus" @click.prevent="deleteStatus"
@click="close" @click="close"
> >
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span> <FAIcon
fixed-width
icon="times"
/><span>{{ $t("status.delete") }}</span>
</button> </button>
<button <button
class="dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@click.prevent="copyLink" @click.prevent="copyLink"
@click="close" @click="close"
> >
<i class="icon-share" /><span>{{ $t("status.copy_link") }}</span> <FAIcon
fixed-width
icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span>
</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> </button>
</div> </div>
</div> </div>
<i <span
slot="trigger" slot="trigger"
class="icon-ellipsis button-icon" class="popover-trigger"
/> >
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</span>
</Popover> </Popover>
</template> </template>
@ -85,13 +138,21 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.icon-ellipsis { .ExtraButtons {
cursor: pointer; /* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
&:hover, .popover-trigger {
.extra-button-popover.open & { position: static;
color: $fallback--text; padding: 10px;
color: var(--text, $fallback--text); margin: -10px;
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
} }
} }
</style> </style>

View file

@ -1,4 +1,14 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faStar } from '@fortawesome/free-solid-svg-icons'
import {
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
library.add(
faStar,
faStarRegular
)
const FavoriteButton = { const FavoriteButton = {
props: ['status', 'loggedIn'], props: ['status', 'loggedIn'],
@ -21,13 +31,6 @@ const FavoriteButton = {
} }
}, },
computed: { computed: {
classes () {
return {
'icon-star-empty': !this.status.favorited,
'icon-star': this.status.favorited,
'animate-spin': this.animated
}
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
} }
} }

View file

@ -1,20 +1,31 @@
<template> <template>
<div v-if="loggedIn"> <div class="FavoriteButton">
<i <button
:class="classes" v-if="loggedIn"
class="button-icon favorite-button fav-active" class="button-unstyled interactive"
:class="status.favorited && '-favorited'"
:title="$t('tool_tip.favorite')" :title="$t('tool_tip.favorite')"
@click.prevent="favorite()" @click.prevent="favorite()"
/> >
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> <FAIcon
</div> class="fa-scale-110 fa-old-padding"
<div v-else> :icon="[status.favorited ? 'fas' : 'far', 'star']"
<i :spin="animated"
:class="classes" />
class="button-icon favorite-button" </button>
:title="$t('tool_tip.favorite')" <span v-else>
/> <FAIcon
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']"
/>
</span>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
class="action-counter"
>
{{ status.fave_num }}
</span>
</div> </div>
</template> </template>
@ -23,18 +34,29 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.fav-active { .FavoriteButton {
cursor: pointer; display: flex;
animation-duration: 0.6s;
&:hover { > :first-child {
color: $fallback--cOrange; padding: 10px;
color: var(--cOrange, $fallback--cOrange); 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: var(--cOrange, $fallback--cOrange);
}
} }
} }
.favorite-button.icon-star {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
</style> </style>

View file

@ -1,3 +1,5 @@
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const FeaturesPanel = { const FeaturesPanel = {
computed: { computed: {
chat: function () { return this.$store.state.instance.chatAvailable }, chat: function () { return this.$store.state.instance.chatAvailable },
@ -6,7 +8,8 @@ const FeaturesPanel = {
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) }
} }
} }

View file

@ -25,6 +25,7 @@
</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>

View file

@ -1,6 +1,6 @@
<template> <template>
<button <button
class="btn btn-default follow-button" class="btn button-default follow-button"
:class="{ toggled: isPressed }" :class="{ toggled: isPressed }"
:disabled="inProgress" :disabled="inProgress"
:title="title" :title="title"

View file

@ -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 btn-default" class="btn button-default"
@click="approveUser" @click="approveUser"
> >
{{ $t('user_card.approve') }} {{ $t('user_card.approve') }}
</button> </button>
<button <button
class="btn btn-default" class="btn button-default"
@click="denyUser" @click="denyUser"
> >
{{ $t('user_card.deny') }} {{ $t('user_card.deny') }}

View file

@ -1,4 +1,12 @@
import { set } from 'vue' import { set } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown
)
export default { export default {
props: [ props: [

View file

@ -41,7 +41,10 @@
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
</option> </option>
</select> </select>
<i class="icon-down-open" /> <FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label> </label>
<input <input
v-if="isCustom" v-if="isCustom"

View file

@ -1,3 +1,11 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
const GlobalNoticeList = { const GlobalNoticeList = {
computed: { computed: {

View file

@ -9,10 +9,15 @@
<div class="notice-message"> <div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }} {{ $t(notice.messageKey, notice.messageArgs) }}
</div> </div>
<i <button
class="button-icon icon-cancel" class="button-unstyled close-notice"
@click="closeNotice(notice)" @click="closeNotice(notice)"
/> >
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div> </div>
</div> </div>
</template> </template>
@ -53,7 +58,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);
i { .svg-inline--fa {
color: var(--alertPopupErrorText, $fallback--text); color: var(--alertPopupErrorText, $fallback--text);
} }
} }
@ -61,17 +66,32 @@
.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);
i { .svg-inline--fa {
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);
i { .svg-inline--fa {
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>

View file

@ -1,5 +1,13 @@
import Cropper from 'cropperjs' import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css' import 'cropperjs/dist/cropper.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
const ImageCropper = { const ImageCropper = {
props: { props: {
@ -43,8 +51,7 @@ const ImageCropper = {
cropper: undefined, cropper: undefined,
dataUrl: undefined, dataUrl: undefined,
filename: undefined, filename: undefined,
submitting: false, submitting: false
submitError: null
} }
}, },
computed: { computed: {
@ -56,9 +63,6 @@ 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: {
@ -72,12 +76,8 @@ 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,9 +103,6 @@ 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 () {

View file

@ -11,39 +11,30 @@
</div> </div>
<div class="image-cropper-buttons-wrapper"> <div class="image-cropper-buttons-wrapper">
<button <button
class="btn" class="button-default btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="submit()" @click="submit()"
v-text="saveText" v-text="saveText"
/> />
<button <button
class="btn" class="button-default btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="destroy" @click="destroy"
v-text="cancelText" v-text="cancelText"
/> />
<button <button
class="btn" class="button-default btn"
type="button" type="button"
:disabled="submitting" :disabled="submitting"
@click="submit(false)" @click="submit(false)"
v-text="saveWithoutCroppingText" v-text="saveWithoutCroppingText"
/> />
<i <FAIcon
v-if="submitting" v-if="submitting"
class="icon-spin4 animate-spin" spin
/> icon="circle-notch"
</div>
<div
v-if="submitError"
class="alert error"
>
{{ submitErrorMsg }}
<i
class="button-icon icon-cancel"
@click="clearError"
/> />
</div> </div>
</div> </div>

View file

@ -1,3 +1,14 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch,
faTimes
)
const Importer = { const Importer = {
props: { props: {
submitHandler: { submitHandler: {

View file

@ -7,27 +7,29 @@
@change="change" @change="change"
> >
</form> </form>
<i <FAIcon
v-if="submitting" v-if="submitting"
class="icon-spin4 animate-spin importer-uploading" class="importer-uploading"
spin
icon="circle-notch"
/> />
<button <button
v-else v-else
class="btn btn-default" class="btn button-default"
@click="submit" @click="submit"
> >
{{ submitButtonLabel }} {{ submitButtonLabel }}
</button> </button>
<div v-if="success"> <div v-if="success">
<i <FAIcon
class="icon-cross" icon="times"
@click="dismiss" @click="dismiss"
/> />
<p>{{ successMessage }}</p> <p>{{ successMessage }}</p>
</div> </div>
<div v-else-if="error"> <div v-else-if="error">
<i <FAIcon
class="icon-cross" icon="times"
@click="dismiss" @click="dismiss"
/> />
<p>{{ errorMessage }}</p> <p>{{ errorMessage }}</p>

View file

@ -12,31 +12,39 @@
v-model="language" v-model="language"
> >
<option <option
v-for="(langCode, i) in languageCodes" v-for="lang in languages"
:key="langCode" :key="lang.code"
:value="langCode" :value="lang.code"
> >
{{ languageNames[i] }} {{ lang.name }}
</option> </option>
</select> </select>
<i class="icon-down-open" /> <FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label> </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 { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown
)
export default { export default {
computed: { computed: {
languageCodes () { languages () {
return languagesObject.languages return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
},
languageNames () {
return _.map(this.languageCodes, this.getLanguageName)
}, },
language: { language: {
@ -50,11 +58,13 @@ export default {
methods: { methods: {
getLanguageName (code) { getLanguageName (code) {
const specialLanguageNames = { const specialLanguageNames = {
'ja': 'Japanese (日本語)', 'ja_easy': 'やさしいにほんご',
'ja_easy': 'Japanese (やさしいにほんご)', 'zh': '简体中文',
'zh': 'Chinese (简体中文)' 'zh_Hant': '繁體中文'
} }
return specialLanguageNames[code] || ISO6391.getName(code) const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
const browserLocale = localeService.internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
} }
} }
} }

View file

@ -1,3 +1,5 @@
import { mapGetters } from 'vuex'
const LinkPreview = { const LinkPreview = {
name: 'LinkPreview', name: 'LinkPreview',
props: [ props: [
@ -15,11 +17,20 @@ 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.nsfw && this.size !== 'hide' return this.card.image && !this.censored && 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) {

View file

@ -9,12 +9,17 @@
<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">{{ card.provider_name }}</span> <span class="card-host faint">
<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"
@ -50,10 +55,6 @@
} }
} }
.small-image {
width: 80px;
}
.card-content { .card-content {
max-height: 100%; max-height: 100%;
margin: 0.5em; margin: 0.5em;
@ -76,6 +77,10 @@
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;

View file

@ -1,5 +1,13 @@
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import oauthApi from '../../services/new_api/oauth.js' import oauthApi from '../../services/new_api/oauth.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
const LoginForm = { const LoginForm = {
data: () => ({ data: () => ({

View file

@ -61,7 +61,7 @@
<button <button
:disabled="loggingIn" :disabled="loggingIn"
type="submit" type="submit"
class="btn btn-default" class="btn button-default"
> >
{{ $t('login.login') }} {{ $t('login.login') }}
</button> </button>
@ -76,8 +76,9 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<i <FAIcon
class="button-icon icon-cancel" class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
/> />
</div> </div>

View file

@ -3,6 +3,16 @@ import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue' import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronLeft,
faChevronRight
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronLeft,
faChevronRight
)
const MediaModal = { const MediaModal = {
components: { components: {

View file

@ -34,7 +34,10 @@
class="modal-view-button-arrow modal-view-button-arrow--prev" class="modal-view-button-arrow modal-view-button-arrow--prev"
@click.stop.prevent="goPrev" @click.stop.prevent="goPrev"
> >
<i class="icon-left-open arrow-icon" /> <FAIcon
class="arrow-icon"
icon="chevron-left"
/>
</button> </button>
<button <button
v-if="canNavigate" v-if="canNavigate"
@ -42,7 +45,10 @@
class="modal-view-button-arrow modal-view-button-arrow--next" class="modal-view-button-arrow modal-view-button-arrow--next"
@click.stop.prevent="goNext" @click.stop.prevent="goNext"
> >
<i class="icon-right-open arrow-icon" /> <FAIcon
class="arrow-icon"
icon="chevron-right"
/>
</button> </button>
</Modal> </Modal>
</template> </template>
@ -67,11 +73,21 @@
} }
} }
@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 {

View file

@ -2,6 +2,14 @@
import statusPosterService from '../../services/status_poster/status_poster.service.js' import statusPosterService from '../../services/status_poster/status_poster.service.js'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faUpload,
faCircleNotch
)
const mediaUpload = { const mediaUpload = {
data () { data () {
return { return {

View file

@ -1,30 +1,29 @@
<template> <template>
<div <label
class="media-upload" class="media-upload"
:class="{ disabled: disabled }" :class="{ disabled: disabled }"
:title="$t('tool_tip.media_upload')"
> >
<label <FAIcon
class="label" v-if="uploading"
:title="$t('tool_tip.media_upload')" 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"
> >
<i </label>
v-if="uploading"
class="progress-icon icon-spin4 animate-spin"
/>
<i
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>
@ -33,22 +32,6 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.media-upload { .media-upload {
.label { cursor: pointer;
display: inline-block;
}
.new-icon {
cursor: pointer;
}
.progress-icon {
display: inline-block;
line-height: 0;
&::before {
/* Overriding fontello to achieve the perfect speeeen */
margin: 0;
line-height: 0;
}
}
} }
</style> </style>

View file

@ -1,5 +1,13 @@
import mfaApi from '../../services/new_api/mfa.js' import mfaApi from '../../services/new_api/mfa.js'
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
export default { export default {
data: () => ({ data: () => ({

View file

@ -23,23 +23,25 @@
<div class="form-group"> <div class="form-group">
<div class="login-bottom"> <div class="login-bottom">
<div> <div>
<a <button
href="#" class="button-unstyled -link"
type="button"
@click.prevent="requireTOTP" @click.prevent="requireTOTP"
> >
{{ $t('login.enter_two_factor_code') }} {{ $t('login.enter_two_factor_code') }}
</a> </button>
<br> <br>
<a <button
href="#" class="button-unstyled -link"
type="button"
@click.prevent="abortMFA" @click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</a> </button>
</div> </div>
<button <button
type="submit" type="submit"
class="btn btn-default" class="btn button-default"
> >
{{ $t('general.verify') }} {{ $t('general.verify') }}
</button> </button>
@ -54,8 +56,9 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<i <FAIcon
class="button-icon icon-cancel" class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
/> />
</div> </div>

View file

@ -1,5 +1,14 @@
import mfaApi from '../../services/new_api/mfa.js' import mfaApi from '../../services/new_api/mfa.js'
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
export default { export default {
data: () => ({ data: () => ({
code: null, code: null,

View file

@ -25,23 +25,25 @@
<div class="form-group"> <div class="form-group">
<div class="login-bottom"> <div class="login-bottom">
<div> <div>
<a <button
href="#" class="button-unstyled -link"
type="button"
@click.prevent="requireRecovery" @click.prevent="requireRecovery"
> >
{{ $t('login.enter_recovery_code') }} {{ $t('login.enter_recovery_code') }}
</a> </button>
<br> <br>
<a <button
href="#" class="button-unstyled -link"
type="button"
@click.prevent="abortMFA" @click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</a> </button>
</div> </div>
<button <button
type="submit" type="submit"
class="btn btn-default" class="btn button-default"
> >
{{ $t('general.verify') }} {{ $t('general.verify') }}
</button> </button>
@ -56,8 +58,10 @@
> >
<div class="alert error"> <div class="alert error">
{{ error }} {{ error }}
<i <FAIcon
class="button-icon icon-cancel" size="lg"
class="fa-scale-110 fa-old-padding"
icon="times"
@click="clearError" @click="clearError"
/> />
</div> </div>

View file

@ -3,6 +3,18 @@ import Notifications from '../notifications/notifications.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
faBell,
faBars
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes,
faBell,
faBars
)
const MobileNav = { const MobileNav = {
components: { components: {

View file

@ -1,49 +1,51 @@
<template> <template>
<div> <div
class="MobileNav"
>
<nav <nav
id="nav" id="nav"
class="nav-bar container" class="mobile-nav"
:class="{ 'mobile-hidden': isChat }" :class="{ 'mobile-hidden': isChat }"
@click="scrollToTop()"
> >
<div <div class="item">
class="mobile-inner-nav" <button
@click="scrollToTop()" class="button-unstyled mobile-nav-button"
> @click.stop.prevent="toggleMobileSidebar()"
<div class="item"> >
<a <FAIcon
href="#" class="fa-scale-110 fa-old-padding"
class="mobile-nav-button" icon="bars"
@click.stop.prevent="toggleMobileSidebar()" />
> <div
<i class="button-icon icon-menu" /> v-if="unreadChatCount"
<div class="alert-dot"
v-if="unreadChatCount" />
class="alert-dot" </button>
/> <router-link
</a> v-if="!hideSitename"
<router-link class="site-name"
v-if="!hideSitename" :to="{ name: 'root' }"
class="site-name" active-class="home"
:to="{ name: 'root' }" >
active-class="home" {{ sitename }}
> </router-link>
{{ sitename }} </div>
</router-link> <div class="item right">
</div> <button
<div class="item right"> v-if="currentUser"
<a class="button-unstyled mobile-nav-button"
v-if="currentUser" @click.stop.prevent="openMobileNotifications()"
class="mobile-nav-button" >
href="#" <FAIcon
@click.stop.prevent="openMobileNotifications()" class="fa-scale-110 fa-old-padding"
> icon="bell"
<i class="button-icon icon-bell-alt" /> />
<div <div
v-if="unseenNotificationsCount" v-if="unseenNotificationsCount"
class="alert-dot" class="alert-dot"
/> />
</a> </button>
</div>
</div> </div>
</nav> </nav>
<div <div
@ -59,7 +61,10 @@
class="mobile-nav-button" class="mobile-nav-button"
@click.stop.prevent="closeMobileNotifications()" @click.stop.prevent="closeMobileNotifications()"
> >
<i class="button-icon icon-cancel" /> <FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</a> </a>
</div> </div>
<div <div
@ -84,101 +89,124 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.mobile-inner-nav { .MobileNav {
width: 100%; .mobile-nav {
display: flex; display: grid;
align-items: center; line-height: 50px;
} height: 50px;
grid-template-rows: 50px;
.mobile-nav-button { grid-template-columns: 2fr auto;
display: flex; width: 100%;
justify-content: center; position: fixed;
width: 50px; box-sizing: border-box;
position: relative;
cursor: pointer;
}
.alert-dot {
border-radius: 100%;
height: 8px;
width: 8px;
position: absolute;
left: calc(50% - 4px);
top: calc(50% - 4px);
margin-left: 6px;
margin-top: -6px;
background-color: $fallback--cRed;
background-color: var(--badgeNotification, $fallback--cRed);
}
.mobile-notifications-drawer {
width: 100%;
height: 100vh;
overflow-x: hidden;
position: fixed;
top: 0;
left: 0;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
transition-property: transform;
transition-duration: 0.25s;
transform: translateX(0);
z-index: 1001;
-webkit-overflow-scrolling: touch;
&.closed {
transform: translateX(100%);
} }
}
.mobile-notifications-header { .mobile-inner-nav {
display: flex; width: 100%;
align-items: center; display: flex;
justify-content: space-between; align-items: center;
z-index: 1;
width: 100%;
height: 50px;
line-height: 50px;
position: absolute;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow);
.title {
font-size: 1.3em;
margin-left: 0.6em;
} }
}
.mobile-notifications { .mobile-nav-button {
margin-top: 50px; display: inline-block;
width: 100vw; text-align: center;
height: calc(100vh - 50px); padding: 0 1em;
overflow-x: hidden; position: relative;
overflow-y: scroll; cursor: pointer;
}
color: $fallback--text; .site-name {
color: var(--text, $fallback--text); padding: 0 .3em;
background-color: $fallback--bg; display: inline-block;
background-color: var(--bg, $fallback--bg); }
.notifications { .item {
padding: 0; /* moslty just to get rid of extra whitespaces */
border-radius: 0; display: flex;
box-shadow: none; }
.panel {
border-radius: 0; .alert-dot {
margin: 0; border-radius: 100%;
box-shadow: none; height: 8px;
width: 8px;
position: absolute;
left: calc(50% - 4px);
top: calc(50% - 4px);
margin-left: 6px;
margin-top: -6px;
background-color: $fallback--cRed;
background-color: var(--badgeNotification, $fallback--cRed);
}
.mobile-notifications-drawer {
width: 100%;
height: 100vh;
overflow-x: hidden;
position: fixed;
top: 0;
left: 0;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
transition-property: transform;
transition-duration: 0.25s;
transform: translateX(0);
z-index: 1001;
-webkit-overflow-scrolling: touch;
&.closed {
transform: translateX(100%);
} }
.panel:after { }
border-radius: 0;
.mobile-notifications-header {
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1;
width: 100%;
height: 50px;
line-height: 50px;
position: absolute;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow);
.title {
font-size: 1.3em;
margin-left: 0.6em;
} }
.panel .panel-heading { }
.mobile-notifications {
margin-top: 50px;
width: 100vw;
height: calc(100vh - 50px);
overflow-x: hidden;
overflow-y: scroll;
color: $fallback--text;
color: var(--text, $fallback--text);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.notifications {
padding: 0;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
.panel {
border-radius: 0;
margin: 0;
box-shadow: none;
}
.panel:after {
border-radius: 0;
}
.panel .panel-heading {
border-radius: 0;
box-shadow: none;
}
} }
} }
} }

View file

@ -1,4 +1,12 @@
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPen
} from '@fortawesome/free-solid-svg-icons'
library.add(
faPen
)
const HIDDEN_FOR_PAGES = new Set([ const HIDDEN_FOR_PAGES = new Set([
'chats', 'chats',

View file

@ -1,11 +1,11 @@
<template> <template>
<div v-if="isLoggedIn"> <div v-if="isLoggedIn">
<button <button
class="new-status-button" class="button-default new-status-button"
:class="{ 'hidden': isHidden }" :class="{ 'hidden': isHidden }"
@click="openPostForm" @click="openPostForm"
> >
<i class="icon-edit" /> <FAIcon icon="pen" />
</button> </button>
</div> </div>
</template> </template>
@ -39,7 +39,7 @@
transform: translateY(150%); transform: translateY(150%);
} }
i { svg {
font-size: 1.5em; font-size: 1.5em;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);

View file

@ -1,6 +1,11 @@
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'

View file

@ -12,13 +12,13 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
class="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default dropdown-item"
@click="deleteUserDialog(true)" @click="deleteUserDialog(true)"
> >
{{ $t('user_card.admin_menu.delete_account') }} {{ $t('user_card.admin_menu.delete_account') }}
@ -47,87 +47,88 @@
/> />
<span v-if="hasTagPolicy"> <span v-if="hasTagPolicy">
<button <button
class="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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="dropdown-item" class="button-default 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>
</div> </div>
<button <button
slot="trigger" slot="trigger"
class="btn btn-default btn-block" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled }"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" />
</button> </button>
</Popover> </Popover>
<portal to="modal"> <portal to="modal">
@ -141,13 +142,13 @@
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template slot="footer"> <template slot="footer">
<button <button
class="btn btn-default" class="btn button-default"
@click="deleteUserDialog(false)" @click="deleteUserDialog(false)"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </button>
<button <button
class="btn btn-default danger" class="btn button-default danger"
@click="deleteUser()" @click="deleteUser()"
> >
{{ $t('user_card.admin_menu.delete_user') }} {{ $t('user_card.admin_menu.delete_user') }}
@ -163,25 +164,6 @@
<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 {
@ -189,4 +171,10 @@
height: 100%; height: 100%;
} }
} }
.moderation-tools-button {
svg,i {
font-size: 0.8em;
}
}
</style> </style>

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 btn-default" class="btn button-default"
:disabled="progress" :disabled="progress"
@click="unmuteUser" @click="unmuteUser"
> >
@ -16,7 +16,7 @@
</button> </button>
<button <button
v-else v-else
class="btn btn-default" class="btn button-default"
:disabled="progress" :disabled="progress"
@click="muteUser" @click="muteUser"
> >

View file

@ -1,22 +1,53 @@
import { timelineNames } from '../timeline_menu/timeline_menu.js' import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faChevronDown,
faChevronUp,
faComments,
faBell,
faInfoCircle,
faStream
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faChevronDown,
faChevronUp,
faComments,
faBell,
faInfoCircle,
faStream
)
const NavPanel = { const NavPanel = {
created () { created () {
if (this.currentUser && this.currentUser.locked) { if (this.currentUser && this.currentUser.locked) {
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,

View file

@ -1,45 +1,88 @@
<template> <template>
<div class="nav-panel"> <div class="NavPanel">
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if="currentUser || !privateMode"> <li v-if="currentUser || !privateMode">
<router-link <button
:to="{ name: timelinesRoute }" class="button-unstyled menu-item"
:class="onTimelineRoute && 'router-link-active'" @click="toggleTimelines"
> >
<i class="button-icon icon-home-2" />{{ $t("nav.timelines") }} <FAIcon
</router-link> fixed-width
class="fa-scale-110"
icon="stream"
/>{{ $t("nav.timelines") }}
<FAIcon
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 :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> <router-link
<i class="button-icon icon-bell-alt" />{{ $t("nav.interactions") }} class="menu-item"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="bell"
/>{{ $t("nav.interactions") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && pleromaChatMessagesAvailable"> <li v-if="currentUser && pleromaChatMessagesAvailable">
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> <router-link
class="menu-item"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<div <div
v-if="unreadChatCount" v-if="unreadChatCount"
class="badge badge-notification unread-chat-count" class="badge badge-notification"
> >
{{ unreadChatCount }} {{ unreadChatCount }}
</div> </div>
<i class="button-icon icon-chat" />{{ $t("nav.chats") }} <FAIcon
fixed-width
class="fa-scale-110"
icon="comments"
/>{{ $t("nav.chats") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && currentUser.locked"> <li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }"> <router-link
<i class="button-icon icon-user-plus" />{{ $t("nav.friend_requests") }} class="menu-item"
:to="{ name: 'friend-requests' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="user-plus"
/>{{ $t("nav.friend_requests") }}
<span <span
v-if="followRequestCount > 0" v-if="followRequestCount > 0"
class="badge follow-request-count" class="badge badge-notification"
> >
{{ followRequestCount }} {{ followRequestCount }}
</span> </span>
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: 'about' }"> <router-link
<i class="button-icon icon-info-circled" />{{ $t("nav.about") }} class="menu-item"
:to="{ name: 'about' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="info-circle"
/>{{ $t("nav.about") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -52,84 +95,109 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.nav-panel .panel { .NavPanel {
overflow: hidden; .panel {
box-shadow: var(--panelShadow); overflow: hidden;
} box-shadow: var(--panelShadow);
.nav-panel ul {
list-style: none;
margin: 0;
padding: 0;
}
.follow-request-count {
margin: -6px 10px;
background-color: $fallback--bg;
background-color: var(--input, $fallback--faint);
}
.nav-panel li {
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 0;
&:first-child a {
border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
} }
&:last-child a { ul {
border-bottom-right-radius: $fallback--panelRadius; list-style: none;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); margin: 0;
border-bottom-left-radius: $fallback--panelRadius; padding: 0;
border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
}
}
.nav-panel li:last-child {
border: none;
}
.nav-panel a {
display: block;
padding: 0.8em 0.85em;
&:hover {
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--link;
color: var(--selectedMenuText, $fallback--link);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
} }
&.router-link-active { li {
font-weight: bolder; position: relative;
background-color: $fallback--lightBg; border-bottom: 1px solid;
background-color: var(--selectedMenu, $fallback--lightBg); border-color: $fallback--border;
color: $fallback--text; border-color: var(--border, $fallback--border);
color: var(--selectedMenuText, $fallback--text); padding: 0;
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
&:hover { &:first-child .menu-item {
text-decoration: underline; border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
}
&:last-child .menu-item {
border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius;
border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
} }
} }
}
.nav-panel .button-icon { li:last-child {
margin-right: 0.5em; border: none;
} }
.nav-panel .button-icon:before { .menu-item {
width: 1.1em; display: block;
box-sizing: border-box;
height: 3.5em;
line-height: 3.5em;
padding: 0 1em;
width: 100%;
color: $fallback--link;
color: var(--link, $fallback--link);
&:hover {
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--link;
color: var(--selectedMenuText, $fallback--link);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
}
&.router-link-active {
font-weight: bolder;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
&:hover {
text-decoration: underline;
}
}
}
.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 {
margin-right: 0.8em;
}
.badge {
position: absolute;
right: 0.6rem;
top: 1.25em;
}
} }
</style> </style>

View file

@ -7,6 +7,28 @@ import Timeago from '../timeago/timeago.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCheck,
faTimes,
faStar,
faRetweet,
faUserPlus,
faEyeSlash,
faUser,
faSuitcaseRolling
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCheck,
faTimes,
faStar,
faRetweet,
faUserPlus,
faUser,
faEyeSlash,
faSuitcaseRolling
)
const Notification = { const Notification = {
data () { data () {

View file

@ -1,3 +1,5 @@
@import '../../_variables.scss';
// TODO Copypaste from Status, should unify it somehow // TODO Copypaste from Status, should unify it somehow
.Notification { .Notification {
&.-muted { &.-muted {
@ -49,4 +51,34 @@
display: block; display: block;
} }
} }
.type-icon {
margin: 0 0.1em;
}
&.-type--repeat .type-icon {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
&.-type--follow .type-icon {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
&.-type--follow-request .type-icon {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
&.-type--like .type-icon {
color: orange;
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
&.-type--move .type-icon {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<status <Status
v-if="notification.type === 'mention'" v-if="notification.type === 'mention'"
:compact="true" :compact="true"
:statusoid="notification.status" :statusoid="notification.status"
@ -11,19 +11,23 @@
> >
<small> <small>
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
{{ notification.from_profile.screen_name }} {{ notification.from_profile.screen_name_ui }}
</router-link> </router-link>
</small> </small>
<a <button
href="#" class="button-unstyled unmute"
class="unmute"
@click.prevent="toggleMute" @click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a> >
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</div> </div>
<div <div
v-else v-else
class="non-mention" class="Notification non-mention"
:class="[userClass, { highlighted: userStyle }]" :class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]"
:style="[ userStyle ]" :style="[ userStyle ]"
> >
<a <a
@ -50,36 +54,49 @@
<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" :title="'@'+notification.from_profile.screen_name_ui"
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" :title="'@'+notification.from_profile.screen_name_ui"
>{{ notification.from_profile.name }}</span> >{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'"> <span v-if="notification.type === 'like'">
<i class="fa icon-star lit" /> <FAIcon
class="type-icon"
icon="star"
/>
<small>{{ $t('notifications.favorited_you') }}</small> <small>{{ $t('notifications.favorited_you') }}</small>
</span> </span>
<span v-if="notification.type === 'repeat'"> <span v-if="notification.type === 'repeat'">
<i <FAIcon
class="fa icon-retweet lit" class="type-icon"
icon="retweet"
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
/> />
<small>{{ $t('notifications.repeated_you') }}</small> <small>{{ $t('notifications.repeated_you') }}</small>
</span> </span>
<span v-if="notification.type === 'follow'"> <span v-if="notification.type === 'follow'">
<i class="fa icon-user-plus lit" /> <FAIcon
class="type-icon"
icon="user-plus"
/>
<small>{{ $t('notifications.followed_you') }}</small> <small>{{ $t('notifications.followed_you') }}</small>
</span> </span>
<span v-if="notification.type === 'follow_request'"> <span v-if="notification.type === 'follow_request'">
<i class="fa icon-user lit" /> <FAIcon
class="type-icon"
icon="user"
/>
<small>{{ $t('notifications.follow_request') }}</small> <small>{{ $t('notifications.follow_request') }}</small>
</span> </span>
<span v-if="notification.type === 'move'"> <span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" /> <FAIcon
class="type-icon"
icon="suitcase-rolling"
/>
<small>{{ $t('notifications.migrated_to') }}</small> <small>{{ $t('notifications.migrated_to') }}</small>
</span> </span>
<span v-if="notification.type === 'pleroma:emoji_reaction'"> <span v-if="notification.type === 'pleroma:emoji_reaction'">
@ -116,11 +133,16 @@
/> />
</span> </span>
</div> </div>
<a <button
v-if="needMute" v-if="needMute"
href="#" class="button-unstyled"
@click.prevent="toggleMute" @click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a> >
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</span> </span>
<div <div
v-if="notification.type === 'follow' || notification.type === 'follow_request'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
@ -130,19 +152,21 @@
:to="userProfileLink" :to="userProfileLink"
class="follow-name" class="follow-name"
> >
@{{ notification.from_profile.screen_name }} @{{ notification.from_profile.screen_name_ui }}
</router-link> </router-link>
<div <div
v-if="notification.type === 'follow_request'" v-if="notification.type === 'follow_request'"
style="white-space: nowrap;" style="white-space: nowrap;"
> >
<i <FAIcon
class="icon-ok button-icon follow-request-accept" icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
:title="$t('tool_tip.accept_follow_request')" :title="$t('tool_tip.accept_follow_request')"
@click="approveUser()" @click="approveUser()"
/> />
<i <FAIcon
class="icon-cancel button-icon follow-request-reject" icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
:title="$t('tool_tip.reject_follow_request')" :title="$t('tool_tip.reject_follow_request')"
@click="denyUser()" @click="denyUser()"
/> />
@ -153,7 +177,7 @@
class="move-text" class="move-text"
> >
<router-link :to="targetUserProfileLink"> <router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name }} @{{ notification.target.screen_name_ui }}
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>

View file

@ -0,0 +1,122 @@
<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>
<FAIcon icon="filter" />
</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>

View file

@ -1,15 +1,27 @@
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 { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
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,
@ -28,11 +40,6 @@ 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'
@ -63,14 +70,13 @@ 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', '')
} }
} }

View file

@ -158,37 +158,6 @@
margin-right: .2em; margin-right: .2em;
} }
.icon-retweet.lit {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.icon-user.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-user-plus.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-reply.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-star.lit {
color: orange;
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
.icon-arrow-curved.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.status-content { .status-content {
margin: 0; margin: 0;
max-height: 300px; max-height: 300px;

View file

@ -15,20 +15,14 @@
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="read-button" class="button-default 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
@ -48,20 +42,24 @@
> >
{{ $t('notifications.no_more_notifications') }} {{ $t('notifications.no_more_notifications') }}
</div> </div>
<a <button
v-else-if="!loading" v-else-if="!loading"
href="#" class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()" @click.prevent="fetchOlderNotifications()"
> >
<div class="new-status-notification text-center panel-footer"> <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>
</a> </button>
<div <div
v-else v-else
class="new-status-notification text-center panel-footer" class="new-status-notification text-center panel-footer"
> >
<i class="icon-spin3 animate-spin" /> <FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,12 +1,27 @@
<template> <template>
<div class="panel-loading"> <div class="panel-loading">
<span class="loading-text"> <span class="loading-text">
<i class="icon-spin4 animate-spin" /> <FAIcon
icon="circle-notch"
spin
size="3x"
/>
{{ $t('general.loading') }} {{ $t('general.loading') }}
</span> </span>
</div> </div>
</template> </template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
export default {}
</script>
<style lang="scss"> <style lang="scss">
@import 'src/_variables.scss'; @import 'src/_variables.scss';
@ -18,8 +33,7 @@
font-size: 2em; font-size: 2em;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
.loading-text i { .loading-text svg {
font-size: 3em;
line-height: 0; line-height: 0;
vertical-align: middle; vertical-align: middle;
color: $fallback--text; color: $fallback--text;

View file

@ -1,5 +1,13 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import passwordResetApi from '../../services/new_api/password_reset.js' import passwordResetApi from '../../services/new_api/password_reset.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
const passwordReset = { const passwordReset = {
data: () => ({ data: () => ({

View file

@ -51,9 +51,9 @@
<button <button
:disabled="isPending" :disabled="isPending"
type="submit" type="submit"
class="btn btn-default btn-block" class="btn button-default btn-block"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
</div> </div>
@ -63,10 +63,10 @@
> >
<span>{{ error }}</span> <span>{{ error }}</span>
<a <a
class="button-icon dismiss" class="fa-scale-110 fa-old-padding dismiss"
@click.prevent="dismissError()" @click.prevent="dismissError()"
> >
<i class="icon-cancel" /> <FAIcon icon="times" />
</a> </a>
</p> </p>
</div> </div>
@ -122,7 +122,7 @@
padding-right: 2rem; padding-right: 2rem;
} }
.icon-cancel { .dismiss {
cursor: pointer; cursor: pointer;
} }
} }

View file

@ -42,14 +42,15 @@
:value="index" :value="index"
> >
<label class="option-vote"> <label class="option-vote">
<div>{{ option.title }}</div> <!-- eslint-disable-next-line vue/no-v-html -->
<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 btn-default poll-vote-button" class="btn button-default poll-vote-button"
type="button" type="button"
:disabled="isDisabled" :disabled="isDisabled"
@click="vote" @click="vote"
@ -57,7 +58,12 @@
{{ $t('polls.vote') }} {{ $t('polls.vote') }}
</button> </button>
<div class="total"> <div class="total">
{{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp; <template v-if="typeof poll.voters_count === 'number'">
{{ $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

View file

@ -1,5 +1,17 @@
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 {
faTimes,
faChevronDown,
faPlus
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes,
faChevronDown,
faPlus
)
export default { export default {
name: 'PollForm', name: 'PollForm',

View file

@ -21,24 +21,26 @@
@keydown.enter.stop.prevent="nextOption(index)" @keydown.enter.stop.prevent="nextOption(index)"
> >
</div> </div>
<div <button
v-if="options.length > 2" v-if="options.length > 2"
class="icon-container" class="delete-option button-unstyled -hover-highlight"
@click="deleteOption(index)"
> >
<i <FAIcon icon="times" />
class="icon-cancel" </button>
@click="deleteOption(index)"
/>
</div>
</div> </div>
<a <button
v-if="options.length < maxOptions" v-if="options.length < maxOptions"
class="add-option faint" class="add-option faint button-unstyled -hover-highlight"
@click="addOption" @click="addOption"
> >
<i class="icon-plus" /> <FAIcon
icon="plus"
size="sm"
/>
{{ $t("polls.add_option") }} {{ $t("polls.add_option") }}
</a> </button>
<div class="poll-type-expiry"> <div class="poll-type-expiry">
<div <div
class="poll-type" class="poll-type"
@ -56,7 +58,10 @@
<option value="single">{{ $t('polls.single_choice') }}</option> <option value="single">{{ $t('polls.single_choice') }}</option>
<option value="multiple">{{ $t('polls.multiple_choices') }}</option> <option value="multiple">{{ $t('polls.multiple_choices') }}</option>
</select> </select>
<i class="icon-down-open" /> <FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label> </label>
</div> </div>
<div <div
@ -84,7 +89,10 @@
{{ $t(`time.${unit}_short`, ['']) }} {{ $t(`time.${unit}_short`, ['']) }}
</option> </option>
</select> </select>
<i class="icon-down-open" /> <FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label> </label>
</div> </div>
</div> </div>
@ -104,7 +112,7 @@
.add-option { .add-option {
align-self: flex-start; align-self: flex-start;
padding-top: 0.25em; padding-top: 0.25em;
cursor: pointer; padding-left: 0.1em;
} }
.poll-option { .poll-option {
@ -123,10 +131,10 @@
} }
} }
.icon-container { .delete-option {
// Hack: Move the icon over the input box // Hack: Move the icon over the input box
width: 2em; width: 1.5em;
margin-left: -2em; margin-left: -1.5em;
z-index: 1; z-index: 1;
} }
@ -143,6 +151,7 @@
border: none; border: none;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;
padding-right: 0.75em;
} }
} }

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