forked from AkkomaGang/akkoma-fe
:Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop
This commit is contained in:
commit
7e1b1ec990
50 changed files with 1054 additions and 621 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -3,19 +3,48 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Fixed
|
||||||
|
- Fixed the occasional bug where screen would scroll 1px when typing into a reply form
|
||||||
|
- Fixed custom emoji not working in profile field names
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.1] - 2020-11-11
|
||||||
|
### Fixed
|
||||||
|
- Fixed regression in react popup alignment and overflowing
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.0] - 2020-11-06
|
||||||
### Added
|
### Added
|
||||||
- New option to optimize timeline rendering to make the site more responsive (enabled by default)
|
- 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
|
||||||
|
|
||||||
## [Unreleased patch]
|
### 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 +171,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
|
||||||
|
|
51
src/App.js
51
src/App.js
|
@ -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,6 +10,7 @@ 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'
|
||||||
|
@ -22,7 +22,6 @@ export default {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications,
|
Notifications,
|
||||||
SearchBar,
|
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
|
@ -31,21 +30,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
|
||||||
|
@ -61,28 +53,6 @@ export default {
|
||||||
background () {
|
background () {
|
||||||
return this.currentUser.background_image || this.$store.state.instance.background
|
return this.currentUser.background_image || this.$store.state.instance.background
|
||||||
},
|
},
|
||||||
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 },
|
|
||||||
bgStyle () {
|
bgStyle () {
|
||||||
return {
|
return {
|
||||||
'background-image': `url(${this.background})`
|
'background-image': `url(${this.background})`
|
||||||
|
@ -93,9 +63,7 @@ export default {
|
||||||
'--body-background-image': `url(${this.background})`
|
'--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 &&
|
||||||
|
@ -112,19 +80,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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()
|
||||||
|
|
156
src/App.scss
156
src/App.scss
|
@ -359,119 +359,10 @@ i[class*=icon-], .svg-inline--fa {
|
||||||
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.2em;
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.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-], 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 {
|
|
||||||
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, a svg {
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--topBarLink, $fallback--link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main-router {
|
main-router {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -712,19 +603,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;
|
||||||
|
@ -781,16 +677,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;
|
||||||
|
@ -943,22 +829,6 @@ 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;
|
||||||
|
|
75
src/App.vue
75
src/App.vue
|
@ -9,80 +9,7 @@
|
||||||
:style="bgStyle"
|
: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="mobile-hidden"
|
|
||||||
@toggled="onSearchBarToggled"
|
|
||||||
@click.stop.native
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="mobile-hidden nav-icon"
|
|
||||||
@click.stop="openSettingsModal"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="cog"
|
|
||||||
:title="$t('nav.preferences')"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
|
||||||
class="mobile-hidden nav-icon"
|
|
||||||
target="_blank"
|
|
||||||
><FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="tachometer-alt"
|
|
||||||
:title="$t('nav.administration')"
|
|
||||||
/></a>
|
|
||||||
<a
|
|
||||||
v-if="currentUser"
|
|
||||||
href="#"
|
|
||||||
class="mobile-hidden nav-icon"
|
|
||||||
@click.prevent="logout"
|
|
||||||
><FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="sign-out-alt"
|
|
||||||
: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"
|
||||||
|
|
|
@ -130,6 +130,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')
|
||||||
|
|
|
@ -6,12 +6,13 @@ 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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronLeft
|
faChevronLeft
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
@ -21,6 +22,8 @@ library.add(
|
||||||
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: {
|
||||||
|
@ -34,7 +37,8 @@ const Chat = {
|
||||||
hoveredMessageChainId: undefined,
|
hoveredMessageChainId: undefined,
|
||||||
lastScrollPosition: {},
|
lastScrollPosition: {},
|
||||||
scrollableContainerHeight: '100%',
|
scrollableContainerHeight: '100%',
|
||||||
errorLoadingChat: false
|
errorLoadingChat: false,
|
||||||
|
messageRetriers: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -104,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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -210,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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -218,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)
|
||||||
|
@ -235,12 +242,18 @@ const Chat = {
|
||||||
} 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
|
||||||
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({
|
||||||
|
@ -274,6 +287,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 })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -302,23 +323,7 @@ const Chat = {
|
||||||
})
|
})
|
||||||
this.fetchChat({ isFirstFetch: true })
|
this.fetchChat({ isFirstFetch: true })
|
||||||
},
|
},
|
||||||
sendMessage ({ status, media }) {
|
handleAttachmentPosting () {
|
||||||
const params = {
|
|
||||||
id: this.currentChat.id,
|
|
||||||
content: status
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media[0]) {
|
|
||||||
params.mediaId = media[0].id
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.backendInteractor.sendChatMessage(params)
|
|
||||||
.then(data => {
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
messages: [data],
|
|
||||||
updateMaxId: false
|
|
||||||
}).then(() => {
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||||
|
@ -328,16 +333,64 @@ const Chat = {
|
||||||
}, SAFE_RESIZE_TIME_OFFSET)
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
sendMessage ({ status, media, idempotencyKey }) {
|
||||||
|
const params = {
|
||||||
|
id: this.currentChat.id,
|
||||||
|
content: status,
|
||||||
|
idempotencyKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media[0]) {
|
||||||
|
params.mediaId = media[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
this.$store.dispatch('addChatMessages', {
|
||||||
|
chatId: this.currentChat.id,
|
||||||
|
updateMaxId: false,
|
||||||
|
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||||
})
|
})
|
||||||
|
|
||||||
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 } })
|
||||||
|
|
|
@ -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,8 +58,10 @@
|
||||||
|
|
||||||
.go-back-button {
|
.go-back-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 1.7em;
|
width: 28px;
|
||||||
margin-left: 0.3em;
|
text-align: center;
|
||||||
|
padding: 0.6em;
|
||||||
|
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jump-to-bottom-button {
|
.jump-to-bottom-button {
|
||||||
|
@ -74,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);
|
||||||
|
@ -136,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 {
|
||||||
|
|
|
@ -80,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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,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);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
|
|
||||||
.go-back-button {
|
.go-back-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 1.7em;
|
width: 28px;
|
||||||
margin-left: 0.3em;
|
text-align: center;
|
||||||
|
padding: 0.6em;
|
||||||
|
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
89
src/components/desktop_nav/desktop_nav.js
Normal file
89
src/components/desktop_nav/desktop_nav.js
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
src/components/desktop_nav/desktop_nav.scss
Normal file
112
src/components/desktop_nav/desktop_nav.scss
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.DesktopNav {
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
&, 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;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a svg {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/components/desktop_nav/desktop_nav.vue
Normal file
79
src/components/desktop_nav/desktop_nav.vue
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<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
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="nav-icon"
|
||||||
|
@click.stop="openSettingsModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="cog"
|
||||||
|
:title="$t('nav.preferences')"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
|
<a
|
||||||
|
v-if="currentUser"
|
||||||
|
href="#"
|
||||||
|
class="nav-icon"
|
||||||
|
@click.prevent="logout"
|
||||||
|
><FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="sign-out-alt"
|
||||||
|
:title="$t('login.logout')"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
<script src="./desktop_nav.js"></script>
|
||||||
|
|
||||||
|
<style src="./desktop_nav.scss" lang="scss"></style>
|
|
@ -16,7 +16,6 @@
|
||||||
@click.prevent="muteConversation"
|
@click.prevent="muteConversation"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||||
|
@ -27,7 +26,6 @@
|
||||||
@click.prevent="unmuteConversation"
|
@click.prevent="unmuteConversation"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||||
|
@ -39,7 +37,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="thumbtack"
|
icon="thumbtack"
|
||||||
/><span>{{ $t("status.pin") }}</span>
|
/><span>{{ $t("status.pin") }}</span>
|
||||||
|
@ -51,7 +48,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="thumbtack"
|
icon="thumbtack"
|
||||||
/><span>{{ $t("status.unpin") }}</span>
|
/><span>{{ $t("status.unpin") }}</span>
|
||||||
|
@ -63,7 +59,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
:icon="['far', 'bookmark']"
|
:icon="['far', 'bookmark']"
|
||||||
/><span>{{ $t("status.bookmark") }}</span>
|
/><span>{{ $t("status.bookmark") }}</span>
|
||||||
|
@ -75,7 +70,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="bookmark"
|
icon="bookmark"
|
||||||
/><span>{{ $t("status.unbookmark") }}</span>
|
/><span>{{ $t("status.unbookmark") }}</span>
|
||||||
|
@ -87,7 +81,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="times"
|
icon="times"
|
||||||
/><span>{{ $t("status.delete") }}</span>
|
/><span>{{ $t("status.delete") }}</span>
|
||||||
|
@ -98,7 +91,6 @@
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
size="md"
|
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="share-alt"
|
icon="share-alt"
|
||||||
/><span>{{ $t("status.copy_link") }}</span>
|
/><span>{{ $t("status.copy_link") }}</span>
|
||||||
|
@ -109,7 +101,6 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="ExtraButtons fa-scale-110 fa-old-padding"
|
class="ExtraButtons fa-scale-110 fa-old-padding"
|
||||||
icon="ellipsis-h"
|
icon="ellipsis-h"
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<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 }"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mobile-inner-nav"
|
|
||||||
@click="scrollToTop()"
|
@click="scrollToTop()"
|
||||||
>
|
>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
@ -50,7 +49,6 @@
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div
|
<div
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
|
@ -93,20 +91,43 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.mobile-inner-nav {
|
.MobileNav {
|
||||||
|
.mobile-nav {
|
||||||
|
display: grid;
|
||||||
|
line-height: 50px;
|
||||||
|
height: 50px;
|
||||||
|
grid-template-rows: 50px;
|
||||||
|
grid-template-columns: 2fr auto;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-inner-nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav-button {
|
.mobile-nav-button {
|
||||||
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 1em;
|
padding: 0 1em;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-dot {
|
.site-name {
|
||||||
|
padding: 0 .3em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
/* moslty just to get rid of extra whitespaces */
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-dot {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
@ -117,9 +138,9 @@
|
||||||
margin-top: -6px;
|
margin-top: -6px;
|
||||||
background-color: $fallback--cRed;
|
background-color: $fallback--cRed;
|
||||||
background-color: var(--badgeNotification, $fallback--cRed);
|
background-color: var(--badgeNotification, $fallback--cRed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-notifications-drawer {
|
.mobile-notifications-drawer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@ -137,9 +158,9 @@
|
||||||
&.closed {
|
&.closed {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-notifications-header {
|
.mobile-notifications-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -158,9 +179,9 @@
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
margin-left: 0.6em;
|
margin-left: 0.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-notifications {
|
.mobile-notifications {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
|
@ -189,6 +210,7 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
|
<router-link :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>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
/>{{ $t("nav.friend_requests") }}
|
/>{{ $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>
|
||||||
|
@ -84,13 +84,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.follow-request-count {
|
|
||||||
vertical-align: baseline;
|
|
||||||
background-color: $fallback--bg;
|
|
||||||
background-color: var(--input, $fallback--faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
position: relative;
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
|
@ -154,5 +149,11 @@
|
||||||
.fa-scale-110 {
|
.fa-scale-110 {
|
||||||
margin-right: 0.8em;
|
margin-right: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.6rem;
|
||||||
|
top: 1.25em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
library.add(
|
library.add(
|
||||||
faCircleNotch
|
faCircleNotch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export default {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -75,7 +75,8 @@ const PostStatusForm = {
|
||||||
'autoFocus',
|
'autoFocus',
|
||||||
'fileLimit',
|
'fileLimit',
|
||||||
'submitOnEnter',
|
'submitOnEnter',
|
||||||
'emojiPickerPlacement'
|
'emojiPickerPlacement',
|
||||||
|
'optimisticPosting'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
|
@ -272,7 +273,7 @@ const PostStatusForm = {
|
||||||
if (this.preview) this.previewStatus()
|
if (this.preview) this.previewStatus()
|
||||||
},
|
},
|
||||||
async postStatus (event, newStatus, opts = {}) {
|
async postStatus (event, newStatus, opts = {}) {
|
||||||
if (this.posting) { return }
|
if (this.posting && !this.optimisticPosting) { return }
|
||||||
if (this.disableSubmit) { return }
|
if (this.disableSubmit) { return }
|
||||||
if (this.emojiInputShown) { return }
|
if (this.emojiInputShown) { return }
|
||||||
if (this.submitOnEnter) {
|
if (this.submitOnEnter) {
|
||||||
|
@ -280,6 +281,8 @@ const PostStatusForm = {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
|
||||||
|
|
||||||
if (this.emptyStatus) {
|
if (this.emptyStatus) {
|
||||||
this.error = this.$t('post_status.empty_status_error')
|
this.error = this.$t('post_status.empty_status_error')
|
||||||
return
|
return
|
||||||
|
@ -528,7 +531,7 @@ const PostStatusForm = {
|
||||||
!(isFormBiggerThanScroller &&
|
!(isFormBiggerThanScroller &&
|
||||||
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
|
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
|
||||||
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
|
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
|
||||||
const targetScroll = currentScroll + totalDelta
|
const targetScroll = Math.round(currentScroll + totalDelta)
|
||||||
|
|
||||||
if (scrollerRef === window) {
|
if (scrollerRef === window) {
|
||||||
scrollerRef.scroll(0, targetScroll)
|
scrollerRef.scroll(0, targetScroll)
|
||||||
|
|
|
@ -85,12 +85,17 @@
|
||||||
{{ $t('post_status.preview') }}
|
{{ $t('post_status.preview') }}
|
||||||
<FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" />
|
<FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" />
|
||||||
</a>
|
</a>
|
||||||
<FAIcon
|
<div
|
||||||
v-show="previewLoading"
|
v-show="previewLoading"
|
||||||
|
class="preview-spinner"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-old-padding"
|
||||||
spin
|
spin
|
||||||
icon="circle-notch"
|
icon="circle-notch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showPreview"
|
v-if="showPreview"
|
||||||
class="preview-container"
|
class="preview-container"
|
||||||
|
@ -124,7 +129,7 @@
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
:disabled="posting"
|
:disabled="posting && !optimisticPosting"
|
||||||
size="1"
|
size="1"
|
||||||
class="form-post-subject"
|
class="form-post-subject"
|
||||||
>
|
>
|
||||||
|
@ -150,7 +155,7 @@
|
||||||
:placeholder="placeholder || $t('post_status.default')"
|
:placeholder="placeholder || $t('post_status.default')"
|
||||||
rows="1"
|
rows="1"
|
||||||
cols="1"
|
cols="1"
|
||||||
:disabled="posting"
|
:disabled="posting && !optimisticPosting"
|
||||||
class="form-post-body"
|
class="form-post-body"
|
||||||
:class="{ 'scrollable-form': !!maxHeight }"
|
:class="{ 'scrollable-form': !!maxHeight }"
|
||||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||||
|
@ -383,12 +388,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-heading {
|
.preview-heading {
|
||||||
padding-left: 0.5em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-toggle {
|
.preview-toggle {
|
||||||
|
flex: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
placement="top"
|
placement="top"
|
||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
class="react-button-popover"
|
class="react-button-popover"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
slot="content"
|
slot="content"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
<div class="reaction-picker-filter">
|
<div class="reaction-picker-filter">
|
||||||
<input
|
<input
|
||||||
v-model="filterWord"
|
v-model="filterWord"
|
||||||
|
size="1"
|
||||||
:placeholder="$t('emoji.search_emoji')"
|
:placeholder="$t('emoji.search_emoji')"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,12 +38,13 @@
|
||||||
<div class="reaction-bottom-fader" />
|
<div class="reaction-bottom-fader" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span slot="trigger">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
slot="trigger"
|
|
||||||
class="fa-scale-110 fa-old-padding add-reaction-button"
|
class="fa-scale-110 fa-old-padding add-reaction-button"
|
||||||
:icon="['far', 'smile-beam']"
|
:icon="['far', 'smile-beam']"
|
||||||
:title="$t('tool_tip.add_reaction')"
|
:title="$t('tool_tip.add_reaction')"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<div class="search-bar-container">
|
class="SearchBar"
|
||||||
|
:class="{ '-expanded': !hidden }"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
v-if="hidden"
|
v-if="hidden"
|
||||||
href="#"
|
href="#"
|
||||||
|
@ -41,7 +43,6 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./search_bar.js"></script>
|
<script src="./search_bar.js"></script>
|
||||||
|
@ -49,21 +50,23 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.search-bar-container {
|
.SearchBar {
|
||||||
max-width: 100%;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
&.-expanded {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.search-bar-input,
|
.search-bar-input,
|
||||||
.search-button {
|
.search-button {
|
||||||
height: 29px;
|
height: 29px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar-input {
|
.search-bar-input {
|
||||||
// TODO: do this properly without a rough guesstimate of 2 icons + paddings
|
flex: 1 0 auto;
|
||||||
max-width: calc(100% - 30px - 30px - 20px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-icon {
|
.cancel-icon {
|
||||||
|
|
|
@ -126,6 +126,8 @@ library.add(
|
||||||
faRetweet,
|
faRetweet,
|
||||||
faReply
|
faReply
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export default {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
/> {{ $t("nav.chats") }}
|
/> {{ $t("nav.chats") }}
|
||||||
<span
|
<span
|
||||||
v-if="unreadChatCount"
|
v-if="unreadChatCount"
|
||||||
class="badge badge-notification unread-chat-count"
|
class="badge badge-notification"
|
||||||
>
|
>
|
||||||
{{ unreadChatCount }}
|
{{ unreadChatCount }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
/> {{ $t("nav.friend_requests") }}
|
/> {{ $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>
|
||||||
|
@ -272,12 +272,11 @@
|
||||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||||
--icon: var(--popoverIcon, $fallback--icon);
|
--icon: var(--popoverIcon, $fallback--icon);
|
||||||
|
|
||||||
.follow-request-count {
|
.badge {
|
||||||
vertical-align: baseline;
|
position: absolute;
|
||||||
background-color: $fallback--bg;
|
right: 0.7rem;
|
||||||
background-color: var(--input, $fallback--faint);
|
top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-drawer-logo-wrapper {
|
.side-drawer-logo-wrapper {
|
||||||
|
|
|
@ -7,8 +7,9 @@ $status-margin: 0.75em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--still-image-img: visible;
|
--_still-image-img-visibility: visible;
|
||||||
--still-image-canvas: hidden;
|
--_still-image-canvas-visibility: hidden;
|
||||||
|
--_still-image-label-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-focused {
|
&.-focused {
|
||||||
|
@ -58,6 +59,15 @@ $status-margin: 0.75em;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._misclick-prevention & {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.attachments {
|
||||||
|
pointer-events: initial;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.left-side {
|
.left-side {
|
||||||
margin-right: $status-margin;
|
margin-right: $status-margin;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
visibility: var(--still-image-canvas, visible);
|
visibility: var(--_still-image-canvas-visibility, visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -66,16 +66,19 @@
|
||||||
border-radius: $fallback--tooltipRadius;
|
border-radius: $fallback--tooltipRadius;
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
visibility: var(--still-image-label-visibility, visible);
|
visibility: var(--_still-image-label-visibility, visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover canvas {
|
&:hover canvas {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover::before,
|
&:hover::before {
|
||||||
|
visibility: var(--_still-image-label-visibility, hidden);
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
visibility: var(--still-image-img, hidden);
|
visibility: var(--_still-image-img-visibility, hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover img {
|
&:hover img {
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
width: 100%;
|
margin: 0.2em auto;
|
||||||
margin: 0.2em 0;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Status from '../status/status.vue'
|
||||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||||
import Conversation from '../conversation/conversation.vue'
|
import Conversation from '../conversation/conversation.vue'
|
||||||
import TimelineMenu from '../timeline_menu/timeline_menu.vue'
|
import TimelineMenu from '../timeline_menu/timeline_menu.vue'
|
||||||
import { throttle, keyBy } from 'lodash'
|
import { debounce, throttle, keyBy } from 'lodash'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
@ -40,7 +40,8 @@ const Timeline = {
|
||||||
paused: false,
|
paused: false,
|
||||||
unfocused: false,
|
unfocused: false,
|
||||||
bottomedOut: false,
|
bottomedOut: false,
|
||||||
virtualScrollIndex: 0
|
virtualScrollIndex: 0,
|
||||||
|
blockingClicks: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -70,8 +71,10 @@ const Timeline = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
classes () {
|
classes () {
|
||||||
|
let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
|
||||||
|
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
|
||||||
return {
|
return {
|
||||||
root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []),
|
root: rootClasses,
|
||||||
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
|
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
|
||||||
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
||||||
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
||||||
|
@ -130,6 +133,15 @@ const Timeline = {
|
||||||
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
stopBlockingClicks: debounce(function () {
|
||||||
|
this.blockingClicks = false
|
||||||
|
}, 1000),
|
||||||
|
blockClicksTemporarily () {
|
||||||
|
if (!this.blockingClicks) {
|
||||||
|
this.blockingClicks = true
|
||||||
|
}
|
||||||
|
this.stopBlockingClicks()
|
||||||
|
},
|
||||||
handleShortKey (e) {
|
handleShortKey (e) {
|
||||||
// Ignore when input fields are focused
|
// Ignore when input fields are focused
|
||||||
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
|
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
|
||||||
|
@ -141,6 +153,7 @@ const Timeline = {
|
||||||
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
||||||
this.fetchOlderStatuses()
|
this.fetchOlderStatuses()
|
||||||
} else {
|
} else {
|
||||||
|
this.blockClicksTemporarily()
|
||||||
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
|
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
|
||||||
this.paused = false
|
this.paused = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="[classes.root, 'timeline']">
|
<div :class="[classes.root, 'Timeline']">
|
||||||
<div :class="classes.header">
|
<div :class="classes.header">
|
||||||
<TimelineMenu v-if="!embedded" />
|
<TimelineMenu v-if="!embedded" />
|
||||||
<div
|
<div
|
||||||
|
@ -107,10 +107,14 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.timeline {
|
.Timeline {
|
||||||
.loadmore-text {
|
.loadmore-text {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-blocked {
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-heading {
|
.timeline-heading {
|
||||||
|
|
|
@ -20,11 +20,14 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
--still-image-label-visibility: hidden;
|
--_avatarShadowBox: var(--avatarStatusShadow);
|
||||||
|
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
||||||
|
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
||||||
|
--_still-image-label-visibility: hidden;
|
||||||
|
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
box-shadow: var(--avatarStatusShadow);
|
box-shadow: var(--_avatarShadowBox);
|
||||||
border-radius: $fallback--avatarRadius;
|
border-radius: $fallback--avatarRadius;
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
|
||||||
|
@ -34,8 +37,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.better-shadow {
|
&.better-shadow {
|
||||||
box-shadow: var(--avatarStatusShadowInset);
|
box-shadow: var(--_avatarShadowInset);
|
||||||
filter: var(--avatarStatusShadowFilter)
|
filter: var(--_avatarShadowFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animated::before {
|
&.animated::before {
|
||||||
|
|
|
@ -282,6 +282,11 @@
|
||||||
.user-card {
|
.user-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&:hover .Avatar {
|
||||||
|
--_still-image-img-visibility: visible;
|
||||||
|
--_still-image-canvas-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
padding: .5em 0;
|
padding: .5em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -382,20 +387,17 @@
|
||||||
max-height: 56px;
|
max-height: 56px;
|
||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
|
--_avatarShadowBox: var(--avatarShadow);
|
||||||
|
--_avatarShadowFilter: var(--avatarShadowFilter);
|
||||||
|
--_avatarShadowInset: var(--avatarShadowInset);
|
||||||
|
|
||||||
flex: 1 0 100%;
|
flex: 1 0 100%;
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
|
|
||||||
box-shadow: var(--avatarShadow);
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .Avatar {
|
|
||||||
--still-image-img: visible;
|
|
||||||
--still-image-canvas: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-avatar-link {
|
&-avatar-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -20,14 +20,13 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
class="user-profile-field"
|
class="user-profile-field"
|
||||||
>
|
>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<dt
|
<dt
|
||||||
:title="user.fields_text[index].name"
|
:title="user.fields_text[index].name"
|
||||||
class="user-profile-field-name"
|
class="user-profile-field-name"
|
||||||
@click.prevent="linkClicked"
|
@click.prevent="linkClicked"
|
||||||
>
|
v-html="field.name"
|
||||||
{{ field.name }}
|
/>
|
||||||
</dt>
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<dd
|
<dd
|
||||||
:title="user.fields_text[index].value"
|
:title="user.fields_text[index].value"
|
||||||
class="user-profile-field-value"
|
class="user-profile-field-value"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"features_panel": {
|
"features_panel": {
|
||||||
"chat": "Babilejo",
|
"chat": "Babilejo",
|
||||||
"gopher": "Gopher",
|
"gopher": "Gopher",
|
||||||
"media_proxy": "Vidaŭdaĵa prokurilo",
|
"media_proxy": "Vidaŭdaĵa retperilo",
|
||||||
"scope_options": "Agordoj de amplekso",
|
"scope_options": "Agordoj de amplekso",
|
||||||
"text_limit": "Limo de teksto",
|
"text_limit": "Limo de teksto",
|
||||||
"title": "Funkcioj",
|
"title": "Funkcioj",
|
||||||
|
@ -33,7 +33,8 @@
|
||||||
"show_more": "Montri plion",
|
"show_more": "Montri plion",
|
||||||
"retry": "Reprovi",
|
"retry": "Reprovi",
|
||||||
"error_retry": "Bonvolu reprovi",
|
"error_retry": "Bonvolu reprovi",
|
||||||
"loading": "Enlegante…"
|
"loading": "Enlegante…",
|
||||||
|
"peek": "Antaŭmontri"
|
||||||
},
|
},
|
||||||
"image_cropper": {
|
"image_cropper": {
|
||||||
"crop_picture": "Tondi bildon",
|
"crop_picture": "Tondi bildon",
|
||||||
|
@ -70,9 +71,9 @@
|
||||||
"friend_requests": "Petoj pri abono",
|
"friend_requests": "Petoj pri abono",
|
||||||
"mentions": "Mencioj",
|
"mentions": "Mencioj",
|
||||||
"dms": "Rektaj mesaĝoj",
|
"dms": "Rektaj mesaĝoj",
|
||||||
"public_tl": "Publika tempolinio",
|
"public_tl": "Publika historio",
|
||||||
"timeline": "Tempolinio",
|
"timeline": "Historio",
|
||||||
"twkn": "La tuta konata reto",
|
"twkn": "Konata reto",
|
||||||
"user_search": "Serĉi uzantojn",
|
"user_search": "Serĉi uzantojn",
|
||||||
"who_to_follow": "Kiun aboni",
|
"who_to_follow": "Kiun aboni",
|
||||||
"preferences": "Agordoj",
|
"preferences": "Agordoj",
|
||||||
|
@ -80,7 +81,8 @@
|
||||||
"search": "Serĉi",
|
"search": "Serĉi",
|
||||||
"interactions": "Interagoj",
|
"interactions": "Interagoj",
|
||||||
"administration": "Administrado",
|
"administration": "Administrado",
|
||||||
"bookmarks": "Legosignoj"
|
"bookmarks": "Legosignoj",
|
||||||
|
"timelines": "Historioj"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Nekonata stato, serĉante ĝin…",
|
"broken_favorite": "Nekonata stato, serĉante ĝin…",
|
||||||
|
@ -107,14 +109,14 @@
|
||||||
"text/html": "HTML"
|
"text/html": "HTML"
|
||||||
},
|
},
|
||||||
"content_warning": "Temo (malnepra)",
|
"content_warning": "Temo (malnepra)",
|
||||||
"default": "Ĵus alvenis al la Universala Kongreso!",
|
"default": "Ĵus alvenis Esperantujon!",
|
||||||
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
|
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
|
||||||
"posting": "Afiŝante",
|
"posting": "Afiŝante",
|
||||||
"scope": {
|
"scope": {
|
||||||
"direct": "Rekta – Afiŝi nur al menciitaj uzantoj",
|
"direct": "Rekta – Afiŝi nur al menciitaj uzantoj",
|
||||||
"private": "Nur abonantoj – Afiŝi nur al abonantoj",
|
"private": "Nur abonantoj – Afiŝi nur al abonantoj",
|
||||||
"public": "Publika – Afiŝi al publikaj tempolinioj",
|
"public": "Publika – Afiŝi al publikaj historioj",
|
||||||
"unlisted": "Nelistigita – Ne afiŝi al publikaj tempolinioj"
|
"unlisted": "Nelistigita – Ne afiŝi al publikaj historioj"
|
||||||
},
|
},
|
||||||
"scope_notice": {
|
"scope_notice": {
|
||||||
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
|
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
|
||||||
|
@ -193,7 +195,7 @@
|
||||||
"foreground": "Malfono",
|
"foreground": "Malfono",
|
||||||
"general": "Ĝenerala",
|
"general": "Ĝenerala",
|
||||||
"hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj",
|
"hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj",
|
||||||
"hide_attachments_in_tl": "Kaŝi kunsendaĵojn en tempolinio",
|
"hide_attachments_in_tl": "Kaŝi kunsendaĵojn en historioj",
|
||||||
"max_thumbnails": "Maksimuma nombro da bildetoj en afiŝo",
|
"max_thumbnails": "Maksimuma nombro da bildetoj en afiŝo",
|
||||||
"hide_isp": "Kaŝi breton propran al nodo",
|
"hide_isp": "Kaŝi breton propran al nodo",
|
||||||
"preload_images": "Antaŭ-enlegi bildojn",
|
"preload_images": "Antaŭ-enlegi bildojn",
|
||||||
|
@ -246,7 +248,7 @@
|
||||||
"profile_banner": "Rubando de profilo",
|
"profile_banner": "Rubando de profilo",
|
||||||
"profile_tab": "Profilo",
|
"profile_tab": "Profilo",
|
||||||
"radii_help": "Agordi fasadan rondigon de randoj (bildere)",
|
"radii_help": "Agordi fasadan rondigon de randoj (bildere)",
|
||||||
"replies_in_timeline": "Respondoj en tempolinio",
|
"replies_in_timeline": "Respondoj en historioj",
|
||||||
"reply_visibility_all": "Montri ĉiujn respondojn",
|
"reply_visibility_all": "Montri ĉiujn respondojn",
|
||||||
"reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj",
|
"reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj",
|
||||||
"reply_visibility_self": "Montri nur respondojn por mi",
|
"reply_visibility_self": "Montri nur respondojn por mi",
|
||||||
|
@ -297,7 +299,12 @@
|
||||||
"older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.",
|
"older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.",
|
||||||
"future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.",
|
"future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.",
|
||||||
"v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.",
|
"v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.",
|
||||||
"upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras."
|
"upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras.",
|
||||||
|
"snapshot_missing": "Neniu momentokopio de haŭto estis en la dosiero, ĝi povas aspekti iom malsame ol oni intencis.",
|
||||||
|
"snapshot_present": "Ĉiuj valoroj estas transpasataj, ĉar momentokopio de haŭto estas enlegita. Vi povas enlegi anstataŭe la aktualajn datumojn de haŭto.",
|
||||||
|
"snapshot_source_mismatch": "Versioj konfliktas: plej probable la fasado estis reirigita kaj ree ĝisdatigita; se vi ŝanĝis la haŭton per pli malnova versio de la fasado, vi probable volas uzi la malnovan version. Alie uzu la novan.",
|
||||||
|
"migration_napshot_gone": "Ial mankis momentokopio; io povus aspekti malsame ol en via memoro.",
|
||||||
|
"migration_snapshot_ok": "Certige, momentokopio de la haŭto enlegiĝis. Vi povas provi enlegi datumojn de la haŭto."
|
||||||
},
|
},
|
||||||
"use_source": "Nova versio",
|
"use_source": "Nova versio",
|
||||||
"use_snapshot": "Malnova versio",
|
"use_snapshot": "Malnova versio",
|
||||||
|
@ -352,10 +359,11 @@
|
||||||
"icons": "Bildsimboloj",
|
"icons": "Bildsimboloj",
|
||||||
"poll": "Grafo de enketo",
|
"poll": "Grafo de enketo",
|
||||||
"underlay": "Subtavolo",
|
"underlay": "Subtavolo",
|
||||||
"popover": "Ŝpruchelpiloj, menuoj",
|
"popover": "Ŝprucaĵoj, menuoj",
|
||||||
"post": "Afiŝoj/Priskriboj de uzantoj",
|
"post": "Afiŝoj/Priskriboj de uzantoj",
|
||||||
"alert_neutral": "Neŭtrala",
|
"alert_neutral": "Neŭtrala",
|
||||||
"alert_warning": "Averto"
|
"alert_warning": "Averto",
|
||||||
|
"toggled": "Ŝaltita"
|
||||||
},
|
},
|
||||||
"radii": {
|
"radii": {
|
||||||
"_tab_label": "Rondeco"
|
"_tab_label": "Rondeco"
|
||||||
|
@ -388,7 +396,8 @@
|
||||||
"buttonPressed": "Butono (premita)",
|
"buttonPressed": "Butono (premita)",
|
||||||
"buttonPressedHover": "Butono (premita kaj je ŝvebo)",
|
"buttonPressedHover": "Butono (premita kaj je ŝvebo)",
|
||||||
"input": "Eniga kampo"
|
"input": "Eniga kampo"
|
||||||
}
|
},
|
||||||
|
"hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}."
|
||||||
},
|
},
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"_tab_label": "Tiparoj",
|
"_tab_label": "Tiparoj",
|
||||||
|
@ -411,7 +420,7 @@
|
||||||
"button": "Butono",
|
"button": "Butono",
|
||||||
"text": "Kelko da pliaj {0} kaj {1}",
|
"text": "Kelko da pliaj {0} kaj {1}",
|
||||||
"mono": "enhavo",
|
"mono": "enhavo",
|
||||||
"input": "Ĵus alvenis al la Universala Kongreso!",
|
"input": "Ĵus alvenis Esperantujon!",
|
||||||
"faint_link": "helpan manlibron",
|
"faint_link": "helpan manlibron",
|
||||||
"fine_print": "Legu nian {0} por nenion utilan ekscii!",
|
"fine_print": "Legu nian {0} por nenion utilan ekscii!",
|
||||||
"header_faint": "Tio estas en ordo",
|
"header_faint": "Tio estas en ordo",
|
||||||
|
@ -420,7 +429,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj",
|
"discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj",
|
||||||
"mutes_and_blocks": "Silentigitoj kaj blokitoj",
|
"mutes_and_blocks": "Blokado kaj silentigoj",
|
||||||
"chatMessageRadius": "Babileja mesaĝo",
|
"chatMessageRadius": "Babileja mesaĝo",
|
||||||
"changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!",
|
"changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!",
|
||||||
"change_email_error": "Eraris ŝanĝo de via retpoŝtadreso.",
|
"change_email_error": "Eraris ŝanĝo de via retpoŝtadreso.",
|
||||||
|
@ -448,7 +457,10 @@
|
||||||
"warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.",
|
"warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.",
|
||||||
"generate_new_recovery_codes": "Estigi novajn rehavajn kodojn",
|
"generate_new_recovery_codes": "Estigi novajn rehavajn kodojn",
|
||||||
"title": "Duobla aŭtentikigo",
|
"title": "Duobla aŭtentikigo",
|
||||||
"otp": "OTP"
|
"otp": "OTP",
|
||||||
|
"wait_pre_setup_otp": "antaŭagordante OTP",
|
||||||
|
"setup_otp": "Agordi OTP",
|
||||||
|
"confirm_and_enable": "Konfirmi kaj ŝalti OTP"
|
||||||
},
|
},
|
||||||
"enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon",
|
"enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon",
|
||||||
"security": "Sekureco",
|
"security": "Sekureco",
|
||||||
|
@ -480,11 +492,11 @@
|
||||||
},
|
},
|
||||||
"import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero",
|
"import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero",
|
||||||
"hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj",
|
"hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj",
|
||||||
"emoji_reactions_on_timeline": "Montri bildosignajn reagojn en la tempolinio",
|
"emoji_reactions_on_timeline": "Montri bildosignajn reagojn en historioj",
|
||||||
"pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto",
|
"pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto",
|
||||||
"domain_mutes": "Retnomoj",
|
"domain_mutes": "Retnomoj",
|
||||||
"notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.",
|
"notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.",
|
||||||
"notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu.",
|
"notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu ĝin.",
|
||||||
"notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj",
|
"notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj",
|
||||||
"notification_setting_privacy": "Privateco",
|
"notification_setting_privacy": "Privateco",
|
||||||
"notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas",
|
"notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas",
|
||||||
|
@ -495,7 +507,14 @@
|
||||||
"backend_version": "Versio de internaĵo",
|
"backend_version": "Versio de internaĵo",
|
||||||
"title": "Versio"
|
"title": "Versio"
|
||||||
},
|
},
|
||||||
"accent": "Emfazo"
|
"accent": "Emfazo",
|
||||||
|
"virtual_scrolling": "Optimumigi bildigon de historioj",
|
||||||
|
"import_mutes_from_a_csv_file": "Enporti silentigojn el CSV-dosiero",
|
||||||
|
"mutes_imported": "Silentigoj enportiĝis! Traktado daŭros iom da tempo.",
|
||||||
|
"mute_import_error": "Eraris enporto de silentigoj",
|
||||||
|
"mute_import": "Enporto de silentigoj",
|
||||||
|
"mute_export_button": "Elportu viajn silentigojn al CSV-dosiero",
|
||||||
|
"mute_export": "Elporto de silentigoj"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"collapse": "Maletendi",
|
"collapse": "Maletendi",
|
||||||
|
@ -503,7 +522,7 @@
|
||||||
"error_fetching": "Eraris ĝisdatigo",
|
"error_fetching": "Eraris ĝisdatigo",
|
||||||
"load_older": "Montri pli malnovajn statojn",
|
"load_older": "Montri pli malnovajn statojn",
|
||||||
"no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti",
|
"no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti",
|
||||||
"repeated": "ripetita",
|
"repeated": "ripetis",
|
||||||
"show_new": "Montri novajn",
|
"show_new": "Montri novajn",
|
||||||
"up_to_date": "Ĝisdata",
|
"up_to_date": "Ĝisdata",
|
||||||
"no_more_statuses": "Neniuj pliaj statoj",
|
"no_more_statuses": "Neniuj pliaj statoj",
|
||||||
|
@ -648,21 +667,22 @@
|
||||||
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
|
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
|
||||||
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
|
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
|
||||||
"media_removal": "Forigo de vidaŭdaĵoj",
|
"media_removal": "Forigo de vidaŭdaĵoj",
|
||||||
"ftl_removal": "Forigo de la historio de «La tuta konata reto»",
|
"ftl_removal": "Forigo el la historio de «La tuta konata reto»",
|
||||||
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
|
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
|
||||||
"quarantine": "Kvaranteno",
|
"quarantine": "Kvaranteno",
|
||||||
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
|
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
|
||||||
"reject": "Rifuzi",
|
"reject": "Rifuzi",
|
||||||
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
|
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
|
||||||
"accept": "Akcepti",
|
"accept": "Akcepti",
|
||||||
"simple_policies": "Specialaj politikoj de la nodo"
|
"simple_policies": "Specialaj politikoj de la nodo",
|
||||||
|
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:"
|
||||||
},
|
},
|
||||||
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
|
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
|
||||||
"keyword": {
|
"keyword": {
|
||||||
"is_replaced_by": "→",
|
"is_replaced_by": "→",
|
||||||
"replace": "Anstataŭigi",
|
"replace": "Anstataŭigi",
|
||||||
"reject": "Rifuzi",
|
"reject": "Rifuzi",
|
||||||
"ftl_removal": "Forigo de la historio de «La tuta konata reto»",
|
"ftl_removal": "Forigo el la historio de «La tuta konata reto»",
|
||||||
"keyword_policies": "Politiko pri ŝlosilvortoj"
|
"keyword_policies": "Politiko pri ŝlosilvortoj"
|
||||||
},
|
},
|
||||||
"federation": "Federado",
|
"federation": "Federado",
|
||||||
|
@ -707,7 +727,8 @@
|
||||||
"pin": "Fiksi al profilo",
|
"pin": "Fiksi al profilo",
|
||||||
"delete": "Forigi staton",
|
"delete": "Forigi staton",
|
||||||
"repeats": "Ripetoj",
|
"repeats": "Ripetoj",
|
||||||
"favorites": "Ŝatataj"
|
"favorites": "Ŝatoj",
|
||||||
|
"status_deleted": "Ĉi tiu afiŝo foriĝis"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"years_short": "{0}j",
|
"years_short": "{0}j",
|
||||||
|
@ -769,7 +790,8 @@
|
||||||
"new": "Nova babilo",
|
"new": "Nova babilo",
|
||||||
"chats": "Babiloj",
|
"chats": "Babiloj",
|
||||||
"delete": "Forigi",
|
"delete": "Forigi",
|
||||||
"you": "Vi:"
|
"you": "Vi:",
|
||||||
|
"message_user": "Mesaĝi al {nickname}"
|
||||||
},
|
},
|
||||||
"password_reset": {
|
"password_reset": {
|
||||||
"password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.",
|
"password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.",
|
||||||
|
@ -791,5 +813,8 @@
|
||||||
"additional_comments": "Aldonaj komentoj",
|
"additional_comments": "Aldonaj komentoj",
|
||||||
"add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:",
|
"add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:",
|
||||||
"title": "Raportante {0}"
|
"title": "Raportante {0}"
|
||||||
|
},
|
||||||
|
"shoutbox": {
|
||||||
|
"title": "Kriujo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
"repeated_you": "转发了你的状态",
|
"repeated_you": "转发了你的状态",
|
||||||
"no_more_notifications": "没有更多的通知",
|
"no_more_notifications": "没有更多的通知",
|
||||||
"reacted_with": "作出了 {0} 的反应",
|
"reacted_with": "作出了 {0} 的反应",
|
||||||
"migrated_to": "迁移到",
|
"migrated_to": "迁移到了",
|
||||||
"follow_request": "想要关注你"
|
"follow_request": "想要关注你"
|
||||||
},
|
},
|
||||||
"polls": {
|
"polls": {
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio": "简介",
|
"bio": "简介",
|
||||||
"email": "电子邮箱",
|
"email": "电子邮箱",
|
||||||
"fullname": "全名",
|
"fullname": "显示名称",
|
||||||
"password_confirm": "确认密码",
|
"password_confirm": "确认密码",
|
||||||
"registration": "注册",
|
"registration": "注册",
|
||||||
"token": "邀请码",
|
"token": "邀请码",
|
||||||
|
@ -322,7 +322,7 @@
|
||||||
"search_user_to_mute": "搜索你想要隐藏的用户",
|
"search_user_to_mute": "搜索你想要隐藏的用户",
|
||||||
"security_tab": "安全",
|
"security_tab": "安全",
|
||||||
"scope_copy": "回复时的复制范围(私信是总是复制的)",
|
"scope_copy": "回复时的复制范围(私信是总是复制的)",
|
||||||
"minimal_scopes_mode": "最小发文范围",
|
"minimal_scopes_mode": "使发文可见范围的选项最少化",
|
||||||
"set_new_avatar": "设置新头像",
|
"set_new_avatar": "设置新头像",
|
||||||
"set_new_profile_background": "设置新的个人资料背景",
|
"set_new_profile_background": "设置新的个人资料背景",
|
||||||
"set_new_profile_banner": "设置新的横幅图片",
|
"set_new_profile_banner": "设置新的横幅图片",
|
||||||
|
|
|
@ -680,7 +680,7 @@
|
||||||
"fullname": "顯示名稱",
|
"fullname": "顯示名稱",
|
||||||
"bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。",
|
"bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。",
|
||||||
"fullname_placeholder": "例如:岩倉玲音",
|
"fullname_placeholder": "例如:岩倉玲音",
|
||||||
"username_placeholder": "例如:玲音",
|
"username_placeholder": "例如:lain",
|
||||||
"new_captcha": "點擊圖片獲取新的驗證碼",
|
"new_captcha": "點擊圖片獲取新的驗證碼",
|
||||||
"captcha": "CAPTCHA",
|
"captcha": "CAPTCHA",
|
||||||
"token": "邀請碼",
|
"token": "邀請碼",
|
||||||
|
|
|
@ -75,12 +75,18 @@ const api = {
|
||||||
} else if (message.event === 'delete') {
|
} else if (message.event === 'delete') {
|
||||||
dispatch('deleteStatusById', message.id)
|
dispatch('deleteStatusById', message.id)
|
||||||
} else if (message.event === 'pleroma:chat_update') {
|
} else if (message.event === 'pleroma:chat_update') {
|
||||||
|
// The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
|
||||||
|
// The cause of the duplicates is the WS event arriving earlier than the HTTP response.
|
||||||
|
// This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
|
||||||
|
// (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
|
||||||
|
setTimeout(() => {
|
||||||
dispatch('addChatMessages', {
|
dispatch('addChatMessages', {
|
||||||
chatId: message.chatUpdate.id,
|
chatId: message.chatUpdate.id,
|
||||||
messages: [message.chatUpdate.lastMessage]
|
messages: [message.chatUpdate.lastMessage]
|
||||||
})
|
})
|
||||||
dispatch('updateChat', { chat: message.chatUpdate })
|
dispatch('updateChat', { chat: message.chatUpdate })
|
||||||
maybeShowChatNotification(store, message.chatUpdate)
|
maybeShowChatNotification(store, message.chatUpdate)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,8 @@ const defaultState = {
|
||||||
openedChats: {},
|
openedChats: {},
|
||||||
openedChatMessageServices: {},
|
openedChatMessageServices: {},
|
||||||
fetcher: undefined,
|
fetcher: undefined,
|
||||||
currentChatId: null
|
currentChatId: null,
|
||||||
|
lastReadMessageId: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getChatById = (state, id) => {
|
const getChatById = (state, id) => {
|
||||||
|
@ -92,9 +93,14 @@ const chats = {
|
||||||
commit('setCurrentChatFetcher', { fetcher: undefined })
|
commit('setCurrentChatFetcher', { fetcher: undefined })
|
||||||
},
|
},
|
||||||
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
||||||
|
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
|
||||||
|
|
||||||
dispatch('resetChatNewMessageCount')
|
dispatch('resetChatNewMessageCount')
|
||||||
commit('readChat', { id })
|
commit('readChat', { id, lastReadId })
|
||||||
|
|
||||||
|
if (isNewMessage) {
|
||||||
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteChatMessage ({ rootState, commit }, value) {
|
deleteChatMessage ({ rootState, commit }, value) {
|
||||||
rootState.api.backendInteractor.deleteChatMessage(value)
|
rootState.api.backendInteractor.deleteChatMessage(value)
|
||||||
|
@ -106,6 +112,9 @@ const chats = {
|
||||||
},
|
},
|
||||||
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
||||||
commit('clearOpenedChats', { commit })
|
commit('clearOpenedChats', { commit })
|
||||||
|
},
|
||||||
|
handleMessageError ({ commit }, value) {
|
||||||
|
commit('handleMessageError', { commit, ...value })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -208,11 +217,16 @@ const chats = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
readChat (state, { id }) {
|
readChat (state, { id, lastReadId }) {
|
||||||
|
state.lastReadMessageId = lastReadId
|
||||||
const chat = getChatById(state, id)
|
const chat = getChatById(state, id)
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.unread = 0
|
chat.unread = 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
handleMessageError (state, { chatId, fakeId, isRetry }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
chatService.handleMessageError(chatMessageService, fakeId, isRetry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,10 @@ const defaultState = {
|
||||||
hideSitename: false,
|
hideSitename: false,
|
||||||
hideUserStats: false,
|
hideUserStats: false,
|
||||||
loginMethod: 'password',
|
loginMethod: 'password',
|
||||||
logo: '/static/logo.png',
|
logo: '/static/logo.svg',
|
||||||
logoMargin: '.2em',
|
logoMargin: '.2em',
|
||||||
logoMask: true,
|
logoMask: true,
|
||||||
|
logoLeft: false,
|
||||||
minimalScopesMode: false,
|
minimalScopesMode: false,
|
||||||
nsfwCensorImage: undefined,
|
nsfwCensorImage: undefined,
|
||||||
postContentType: 'text/plain',
|
postContentType: 'text/plain',
|
||||||
|
|
|
@ -130,7 +130,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
|
||||||
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
||||||
}
|
}
|
||||||
return resolve(json)
|
return resolve(json)
|
||||||
}))
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
return reject(new StatusCodeError(response.status, error, { url, options }, response))
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1225,7 +1229,7 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
'content': content
|
'content': content
|
||||||
}
|
}
|
||||||
|
@ -1234,11 +1238,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
||||||
payload['media_id'] = mediaId
|
payload['media_id'] = mediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
if (idempotencyKey) {
|
||||||
|
headers['idempotency-key'] = idempotencyKey
|
||||||
|
}
|
||||||
|
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
payload: payload,
|
payload: payload,
|
||||||
credentials
|
credentials,
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ import _ from 'lodash'
|
||||||
const empty = (chatId) => {
|
const empty = (chatId) => {
|
||||||
return {
|
return {
|
||||||
idIndex: {},
|
idIndex: {},
|
||||||
|
idempotencyKeyIndex: {},
|
||||||
messages: [],
|
messages: [],
|
||||||
newMessageCount: 0,
|
newMessageCount: 0,
|
||||||
lastSeenTimestamp: 0,
|
lastSeenMessageId: '0',
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
minId: undefined,
|
minId: undefined,
|
||||||
maxId: undefined
|
maxId: undefined
|
||||||
|
@ -13,10 +14,20 @@ const empty = (chatId) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clear = (storage) => {
|
const clear = (storage) => {
|
||||||
storage.idIndex = {}
|
const failedMessageIds = []
|
||||||
storage.messages.splice(0, storage.messages.length)
|
|
||||||
|
for (const message of storage.messages) {
|
||||||
|
if (message.error) {
|
||||||
|
failedMessageIds.push(message.id)
|
||||||
|
} else {
|
||||||
|
delete storage.idIndex[message.id]
|
||||||
|
delete storage.idempotencyKeyIndex[message.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
|
||||||
storage.newMessageCount = 0
|
storage.newMessageCount = 0
|
||||||
storage.lastSeenTimestamp = 0
|
storage.lastSeenMessageId = '0'
|
||||||
storage.minId = undefined
|
storage.minId = undefined
|
||||||
storage.maxId = undefined
|
storage.maxId = undefined
|
||||||
}
|
}
|
||||||
|
@ -37,6 +48,25 @@ const deleteMessage = (storage, messageId) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMessageError = (storage, fakeId, isRetry) => {
|
||||||
|
if (!storage) { return }
|
||||||
|
const fakeMessage = storage.idIndex[fakeId]
|
||||||
|
if (fakeMessage) {
|
||||||
|
fakeMessage.error = true
|
||||||
|
fakeMessage.pending = false
|
||||||
|
if (!isRetry) {
|
||||||
|
// Ensure the failed message doesn't stay at the bottom of the list.
|
||||||
|
const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
|
||||||
|
if (lastPersistedMessage) {
|
||||||
|
const oldId = fakeMessage.id
|
||||||
|
fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
|
||||||
|
storage.idIndex[fakeMessage.id] = fakeMessage
|
||||||
|
delete storage.idIndex[oldId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||||
if (!storage) { return }
|
if (!storage) { return }
|
||||||
for (let i = 0; i < newMessages.length; i++) {
|
for (let i = 0; i < newMessages.length; i++) {
|
||||||
|
@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||||
// sanity check
|
// sanity check
|
||||||
if (message.chat_id !== storage.chatId) { return }
|
if (message.chat_id !== storage.chatId) { return }
|
||||||
|
|
||||||
if (!storage.minId || message.id < storage.minId) {
|
if (message.fakeId) {
|
||||||
|
const fakeMessage = storage.idIndex[message.fakeId]
|
||||||
|
if (fakeMessage) {
|
||||||
|
// In case the same id exists (chat update before POST response)
|
||||||
|
// make sure to remove the older duplicate message.
|
||||||
|
if (storage.idIndex[message.id]) {
|
||||||
|
delete storage.idIndex[message.id]
|
||||||
|
storage.messages = storage.messages.filter(msg => msg.id !== message.id)
|
||||||
|
}
|
||||||
|
Object.assign(fakeMessage, message, { error: false })
|
||||||
|
delete fakeMessage['fakeId']
|
||||||
|
storage.idIndex[fakeMessage.id] = fakeMessage
|
||||||
|
delete storage.idIndex[message.fakeId]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storage.minId || (!message.pending && message.id < storage.minId)) {
|
||||||
storage.minId = message.id
|
storage.minId = message.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!storage.idIndex[message.id]) {
|
if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
|
||||||
if (storage.lastSeenTimestamp < message.created_at) {
|
if (storage.lastSeenMessageId < message.id) {
|
||||||
storage.newMessageCount++
|
storage.newMessageCount++
|
||||||
}
|
}
|
||||||
storage.messages.push(message)
|
|
||||||
storage.idIndex[message.id] = message
|
storage.idIndex[message.id] = message
|
||||||
|
storage.messages.push(storage.idIndex[message.id])
|
||||||
|
storage.idempotencyKeyIndex[message.idempotency_key] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isConfirmation = (storage, message) => {
|
||||||
|
if (!message.idempotency_key) return
|
||||||
|
return storage.idempotencyKeyIndex[message.idempotency_key]
|
||||||
|
}
|
||||||
|
|
||||||
const resetNewMessageCount = (storage) => {
|
const resetNewMessageCount = (storage) => {
|
||||||
if (!storage) { return }
|
if (!storage) { return }
|
||||||
storage.newMessageCount = 0
|
storage.newMessageCount = 0
|
||||||
storage.lastSeenTimestamp = new Date()
|
storage.lastSeenMessageId = storage.maxId
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
|
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
|
||||||
|
@ -76,7 +130,7 @@ const getView = (storage) => {
|
||||||
if (!storage) { return [] }
|
if (!storage) { return [] }
|
||||||
|
|
||||||
const result = []
|
const result = []
|
||||||
const messages = _.sortBy(storage.messages, ['id', 'desc'])
|
const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
|
||||||
const firstMessage = messages[0]
|
const firstMessage = messages[0]
|
||||||
let previousMessage = messages[messages.length - 1]
|
let previousMessage = messages[messages.length - 1]
|
||||||
let currentMessageChainId
|
let currentMessageChainId
|
||||||
|
@ -148,7 +202,8 @@ const ChatService = {
|
||||||
getView,
|
getView,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
resetNewMessageCount,
|
resetNewMessageCount,
|
||||||
clear
|
clear,
|
||||||
|
handleMessageError
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatService
|
export default ChatService
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
|
||||||
export const maybeShowChatNotification = (store, chat) => {
|
export const maybeShowChatNotification = (store, chat) => {
|
||||||
if (!chat.lastMessage) return
|
if (!chat.lastMessage) return
|
||||||
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
|
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
|
||||||
if (store.rootState.users.currentUser.id === chat.lastMessage.account.id) return
|
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
tag: chat.lastMessage.id,
|
tag: chat.lastMessage.id,
|
||||||
|
@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => {
|
||||||
|
|
||||||
showDesktopNotification(store.rootState, opts)
|
showDesktopNotification(store.rootState, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => {
|
||||||
|
const fakeMessage = {
|
||||||
|
content,
|
||||||
|
chat_id: chatId,
|
||||||
|
created_at: new Date(),
|
||||||
|
id: `${new Date().getTime()}`,
|
||||||
|
attachments: attachments,
|
||||||
|
account_id: userId,
|
||||||
|
idempotency_key: idempotencyKey,
|
||||||
|
emojis: [],
|
||||||
|
pending: true,
|
||||||
|
isNormalized: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachments[0]) {
|
||||||
|
fakeMessage.attachment = attachments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeMessage
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const parseUser = (data) => {
|
||||||
output.fields = data.fields
|
output.fields = data.fields
|
||||||
output.fields_html = data.fields.map(field => {
|
output.fields_html = data.fields.map(field => {
|
||||||
return {
|
return {
|
||||||
name: addEmojis(field.name, data.emojis),
|
name: addEmojis(escape(field.name), data.emojis),
|
||||||
value: addEmojis(field.value, data.emojis)
|
value: addEmojis(field.value, data.emojis)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -429,6 +429,9 @@ export const parseChatMessage = (message) => {
|
||||||
} else {
|
} else {
|
||||||
output.attachments = []
|
output.attachments = []
|
||||||
}
|
}
|
||||||
|
output.pending = !!message.pending
|
||||||
|
output.error = false
|
||||||
|
output.idempotency_key = message.idempotency_key
|
||||||
output.isNormalized = true
|
output.isNormalized = true
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,14 @@ export const promiseInterval = (promiseCall, interval) => {
|
||||||
let timeout = null
|
let timeout = null
|
||||||
|
|
||||||
const func = () => {
|
const func = () => {
|
||||||
promiseCall().finally(() => {
|
const promise = promiseCall()
|
||||||
|
// something unexpected happened and promiseCall did not
|
||||||
|
// return a promise, abort the loop.
|
||||||
|
if (!(promise && promise.finally)) {
|
||||||
|
console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
promise.finally(() => {
|
||||||
if (stopped) return
|
if (stopped) return
|
||||||
timeout = window.setTimeout(func, interval)
|
timeout = window.setTimeout(func, interval)
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,9 +10,10 @@
|
||||||
"hideSitename": false,
|
"hideSitename": false,
|
||||||
"hideUserStats": false,
|
"hideUserStats": false,
|
||||||
"loginMethod": "password",
|
"loginMethod": "password",
|
||||||
"logo": "/static/logo.png",
|
"logo": "/static/logo.svg",
|
||||||
"logoMargin": ".1em",
|
"logoMargin": ".1em",
|
||||||
"logoMask": true,
|
"logoMask": true,
|
||||||
|
"logoLeft": false,
|
||||||
"minimalScopesMode": false,
|
"minimalScopesMode": false,
|
||||||
"nsfwCensorImage": "",
|
"nsfwCensorImage": "",
|
||||||
"postContentType": "text/plain",
|
"postContentType": "text/plain",
|
||||||
|
|
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
Before Width: | Height: | Size: 5.8 KiB |
71
static/logo.svg
Normal file
71
static/logo.svg
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4485"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
|
||||||
|
<metadata
|
||||||
|
id="metadata4491">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs4489" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1274"
|
||||||
|
inkscape:window-height="1410"
|
||||||
|
id="namedview4487"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.2636719"
|
||||||
|
inkscape:cx="305.99333"
|
||||||
|
inkscape:cy="304.30809"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="22"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g4612"
|
||||||
|
inkscape:document-rotation="0" />
|
||||||
|
<g
|
||||||
|
id="g4612">
|
||||||
|
<g
|
||||||
|
id="g850"
|
||||||
|
transform="matrix(0.99659595,0,0,0.99659595,0.37313949,0.87143746)">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.175879"
|
||||||
|
d="m 194.75841,124.65165 a 20.449443,20.449443 0 0 0 -20.44944,20.44945 v 242.24725 h 65.28091 v -262.6967 z"
|
||||||
|
id="path4497" />
|
||||||
|
<path
|
||||||
|
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 272.6236,124.65165 V 256 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -110.8989 z"
|
||||||
|
id="path4516" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 272.6236,322.06744 v 65.28091 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -44.83146 z"
|
||||||
|
id="path4516-5" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -42,4 +42,4 @@
|
||||||
<p>So I guess yeah, that's about it. Try to be nice, eh? We're probably all sad here.</p>
|
<p>So I guess yeah, that's about it. Try to be nice, eh? We're probably all sad here.</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />
|
<img src="/static/logo.svg" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />
|
||||||
|
|
|
@ -2,17 +2,20 @@ import chatService from '../../../../../src/services/chat_service/chat_service.j
|
||||||
|
|
||||||
const message1 = {
|
const message1 = {
|
||||||
id: '9wLkdcmQXD21Oy8lEX',
|
id: '9wLkdcmQXD21Oy8lEX',
|
||||||
|
idempotency_key: '1',
|
||||||
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const message2 = {
|
const message2 = {
|
||||||
id: '9wLkdp6ihaOVdNj8Wu',
|
id: '9wLkdp6ihaOVdNj8Wu',
|
||||||
|
idempotency_key: '2',
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
account_id: '9vmRb29zLQReckr5ay',
|
||||||
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const message3 = {
|
const message3 = {
|
||||||
id: '9wLke9zL4Dy4OZR2RM',
|
id: '9wLke9zL4Dy4OZR2RM',
|
||||||
|
idempotency_key: '3',
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
account_id: '9vmRb29zLQReckr5ay',
|
||||||
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
||||||
}
|
}
|
||||||
|
@ -44,10 +47,10 @@ describe('chatService', () => {
|
||||||
|
|
||||||
chatService.resetNewMessageCount(chat)
|
chatService.resetNewMessageCount(chat)
|
||||||
expect(chat.newMessageCount).to.eql(0)
|
expect(chat.newMessageCount).to.eql(0)
|
||||||
|
expect(chat.lastSeenMessageId).to.eql(message2.id)
|
||||||
|
|
||||||
const createdAt = new Date()
|
// Add message with higher id
|
||||||
createdAt.setSeconds(createdAt.getSeconds() + 10)
|
chatService.add(chat, { messages: [ message3 ] })
|
||||||
chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] })
|
|
||||||
expect(chat.newMessageCount).to.eql(1)
|
expect(chat.newMessageCount).to.eql(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue