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/).
|
||||
|
||||
|
||||
## [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
|
||||
- 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 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 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
|
||||
### 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
|
||||
- About page
|
||||
- Added remote user redirect
|
||||
- Bookmarks
|
||||
|
||||
### 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
|
||||
|
||||
### Fixed
|
||||
- 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 NavPanel from './components/nav_panel/nav_panel.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 FeaturesPanel from './components/features_panel/features_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 MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.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 PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
|
@ -22,7 +22,6 @@ export default {
|
|||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications,
|
||||
SearchBar,
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -31,21 +30,14 @@ export default {
|
|||
SideDrawer,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
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')
|
||||
)
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
|
@ -61,28 +53,6 @@ export default {
|
|||
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 () {
|
||||
return {
|
||||
'background-image': `url(${this.background})`
|
||||
|
@ -93,9 +63,7 @@ export default {
|
|||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
},
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
|
@ -112,19 +80,6 @@ export default {
|
|||
}
|
||||
},
|
||||
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 () {
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -712,19 +603,24 @@ nav {
|
|||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
border-radius: 99px;
|
||||
min-width: 22px;
|
||||
max-width: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
max-width: 10em;
|
||||
min-width: 1.7em;
|
||||
height: 1.3em;
|
||||
padding: 0.15em 0.15em;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 0.9em;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.badge-notification {
|
||||
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 {
|
||||
padding: .5em;
|
||||
border: 1px solid $fallback--faint;
|
||||
|
@ -943,22 +829,6 @@ nav {
|
|||
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 {
|
||||
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
|
||||
overflow: hidden;
|
||||
|
|
75
src/App.vue
75
src/App.vue
|
@ -9,80 +9,7 @@
|
|||
:style="bgStyle"
|
||||
/>
|
||||
<MobileNav v-if="isMobileLayout" />
|
||||
<nav
|
||||
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>
|
||||
<DesktopNav v-else />
|
||||
<div class="app-bg-wrapper app-container-wrapper" />
|
||||
<div
|
||||
id="content"
|
||||
|
|
|
@ -130,6 +130,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
||||
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 chatService from '../../services/chat_service/chat_service.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
|
||||
library.add(
|
||||
faChevronDown,
|
||||
|
@ -21,6 +22,8 @@ library.add(
|
|||
const BOTTOMED_OUT_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||
const MARK_AS_READ_DELAY = 1500
|
||||
const MAX_RETRIES = 10
|
||||
|
||||
const Chat = {
|
||||
components: {
|
||||
|
@ -34,7 +37,8 @@ const Chat = {
|
|||
hoveredMessageChainId: undefined,
|
||||
lastScrollPosition: {},
|
||||
scrollableContainerHeight: '100%',
|
||||
errorLoadingChat: false
|
||||
errorLoadingChat: false,
|
||||
messageRetriers: {}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -104,7 +108,7 @@ const Chat = {
|
|||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
this.$nextTick(() => {
|
||||
if (bottomedOutBeforeUpdate) {
|
||||
this.scrollDown({ forceRead: !document.hidden })
|
||||
this.scrollDown()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -210,7 +214,7 @@ const Chat = {
|
|||
this.$nextTick(() => {
|
||||
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
||||
})
|
||||
if (forceRead || this.newMessageCount > 0) {
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
|
@ -218,7 +222,10 @@ const Chat = {
|
|||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||
if (document.hidden) { return }
|
||||
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) {
|
||||
return isBottomedOut(this.$refs.scrollable, offset)
|
||||
|
@ -235,12 +242,18 @@ const Chat = {
|
|||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.jumpToBottomButtonVisible = false
|
||||
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 {
|
||||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 100),
|
||||
}, 200),
|
||||
handleScrollUp (positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
||||
this.$refs.scrollable.scrollTo({
|
||||
|
@ -274,6 +287,14 @@ const Chat = {
|
|||
if (isFirstFetch) {
|
||||
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,42 +323,74 @@ const Chat = {
|
|||
})
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
},
|
||||
sendMessage ({ status, media }) {
|
||||
handleAttachmentPosting () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
// to account for the potential delay in the DOM update.
|
||||
setTimeout(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
}, SAFE_RESIZE_TIME_OFFSET)
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
sendMessage ({ status, media, idempotencyKey }) {
|
||||
const params = {
|
||||
id: this.currentChat.id,
|
||||
content: status
|
||||
content: status,
|
||||
idempotencyKey
|
||||
}
|
||||
|
||||
if (media[0]) {
|
||||
params.mediaId = media[0].id
|
||||
}
|
||||
|
||||
return this.backendInteractor.sendChatMessage(params)
|
||||
const fakeMessage = buildFakeMessage({
|
||||
attachments: media,
|
||||
chatId: this.currentChat.id,
|
||||
content: status,
|
||||
userId: this.currentUser.id,
|
||||
idempotencyKey
|
||||
})
|
||||
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage]
|
||||
}).then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
|
||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
||||
},
|
||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor.sendChatMessage(params)
|
||||
.then(data => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [data],
|
||||
updateMaxId: false
|
||||
}).then(() => {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
// to account for the potential delay in the DOM update.
|
||||
setTimeout(() => {
|
||||
this.updateScrollableContainerHeight()
|
||||
}, SAFE_RESIZE_TIME_OFFSET)
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
updateMaxId: false,
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||
})
|
||||
|
||||
return data
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending message', error)
|
||||
return {
|
||||
error: this.$t('chats.error_sending_message')
|
||||
this.$store.dispatch('handleMessageError', {
|
||||
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 () {
|
||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
min-height: 100%;
|
||||
margin: 0 0 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 {
|
||||
border-radius: 0;
|
||||
|
@ -58,8 +58,10 @@
|
|||
|
||||
.go-back-button {
|
||||
cursor: pointer;
|
||||
margin-right: 1.7em;
|
||||
margin-left: 0.3em;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
padding: 0.6em;
|
||||
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||
}
|
||||
|
||||
.jump-to-bottom-button {
|
||||
|
@ -74,7 +76,7 @@
|
|||
display: flex;
|
||||
justify-content: 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;
|
||||
transition: 0.35s all;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
|
@ -136,11 +138,21 @@
|
|||
}
|
||||
|
||||
.chat-view-heading {
|
||||
box-sizing: border-box;
|
||||
position: static;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
margin-top: 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 {
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
:disable-sensitivity-checkbox="true"
|
||||
:disable-submit="errorLoadingChat || !currentChat"
|
||||
:disable-preview="true"
|
||||
:optimistic-posting="true"
|
||||
:post-handler="sendMessage"
|
||||
:submit-on-enter="!mobileLayout"
|
||||
:preserve-focus="!mobileLayout"
|
||||
|
|
|
@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => {
|
|||
export const scrollableContainerHeight = (inner, header, footer) => {
|
||||
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 class="heading-right" />
|
||||
<div class="time-wrapper">
|
||||
<Timeago
|
||||
:time="chat.updated_at"
|
||||
:auto-update="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-preview">
|
||||
<StatusContent
|
||||
|
@ -35,12 +41,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-wrapper">
|
||||
<Timeago
|
||||
:time="chat.updated_at"
|
||||
:auto-update="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
a {
|
||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
>
|
||||
<div
|
||||
class="media status"
|
||||
:class="{ 'without-attachment': !hasAttachment }"
|
||||
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
||||
style="position: relative"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
|
||||
.go-back-button {
|
||||
cursor: pointer;
|
||||
margin-right: 1.7em;
|
||||
margin-left: 0.3em;
|
||||
width: 28px;
|
||||
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"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||
|
@ -27,7 +26,6 @@
|
|||
@click.prevent="unmuteConversation"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
|
@ -39,7 +37,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.pin") }}</span>
|
||||
|
@ -51,7 +48,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
|
@ -63,7 +59,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
|
@ -75,7 +70,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
|
@ -87,7 +81,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="times"
|
||||
/><span>{{ $t("status.delete") }}</span>
|
||||
|
@ -98,7 +91,6 @@
|
|||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
size="md"
|
||||
fixed-width
|
||||
icon="share-alt"
|
||||
/><span>{{ $t("status.copy_link") }}</span>
|
||||
|
@ -109,7 +101,6 @@
|
|||
<FAIcon
|
||||
class="ExtraButtons fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
size="md"
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
|
|
|
@ -1,55 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="MobileNav"
|
||||
>
|
||||
<nav
|
||||
id="nav"
|
||||
class="nav-bar container"
|
||||
class="mobile-nav"
|
||||
:class="{ 'mobile-hidden': isChat }"
|
||||
@click="scrollToTop()"
|
||||
>
|
||||
<div
|
||||
class="mobile-inner-nav"
|
||||
@click="scrollToTop()"
|
||||
>
|
||||
<div class="item">
|
||||
<a
|
||||
href="#"
|
||||
class="mobile-nav-button"
|
||||
@click.stop.prevent="toggleMobileSidebar()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="bars"
|
||||
/>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</a>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
class="site-name"
|
||||
:to="{ name: 'root' }"
|
||||
active-class="home"
|
||||
>
|
||||
{{ sitename }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<a
|
||||
v-if="currentUser"
|
||||
class="mobile-nav-button"
|
||||
href="#"
|
||||
@click.stop.prevent="openMobileNotifications()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="bell"
|
||||
/>
|
||||
<div
|
||||
v-if="unseenNotificationsCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a
|
||||
href="#"
|
||||
class="mobile-nav-button"
|
||||
@click.stop.prevent="toggleMobileSidebar()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="bars"
|
||||
/>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</a>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
class="site-name"
|
||||
:to="{ name: 'root' }"
|
||||
active-class="home"
|
||||
>
|
||||
{{ sitename }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<a
|
||||
v-if="currentUser"
|
||||
class="mobile-nav-button"
|
||||
href="#"
|
||||
@click.stop.prevent="openMobileNotifications()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="bell"
|
||||
/>
|
||||
<div
|
||||
v-if="unseenNotificationsCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
|
@ -93,100 +91,124 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.mobile-inner-nav {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-nav-button {
|
||||
text-align: center;
|
||||
margin: 0 1em;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alert-dot {
|
||||
border-radius: 100%;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
position: absolute;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
background-color: $fallback--cRed;
|
||||
background-color: var(--badgeNotification, $fallback--cRed);
|
||||
}
|
||||
|
||||
.mobile-notifications-drawer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
transform: translateX(0);
|
||||
z-index: 1001;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&.closed {
|
||||
transform: translateX(100%);
|
||||
.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-notifications-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: absolute;
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--topBarShadow);
|
||||
|
||||
.title {
|
||||
font-size: 1.3em;
|
||||
margin-left: 0.6em;
|
||||
.mobile-inner-nav {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-notifications {
|
||||
margin-top: 50px;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 50px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
.mobile-nav-button {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
padding: 0 1em;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
.site-name {
|
||||
padding: 0 .3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
.item {
|
||||
/* moslty just to get rid of extra whitespaces */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.alert-dot {
|
||||
border-radius: 100%;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
position: absolute;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
background-color: $fallback--cRed;
|
||||
background-color: var(--badgeNotification, $fallback--cRed);
|
||||
}
|
||||
|
||||
.mobile-notifications-drawer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
transform: translateX(0);
|
||||
z-index: 1001;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&.closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.panel:after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.mobile-notifications-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: absolute;
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--topBarShadow);
|
||||
|
||||
.title {
|
||||
font-size: 1.3em;
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
.panel .panel-heading {
|
||||
}
|
||||
|
||||
.mobile-notifications {
|
||||
margin-top: 50px;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 50px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
|
||||
.notifications {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.panel:after {
|
||||
border-radius: 0;
|
||||
}
|
||||
.panel .panel-heading {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
class="badge badge-notification unread-chat-count"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ unreadChatCount }}
|
||||
</div>
|
||||
|
@ -47,7 +47,7 @@
|
|||
/>{{ $t("nav.friend_requests") }}
|
||||
<span
|
||||
v-if="followRequestCount > 0"
|
||||
class="badge follow-request-count"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ followRequestCount }}
|
||||
</span>
|
||||
|
@ -84,13 +84,8 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.follow-request-count {
|
||||
vertical-align: baseline;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--input, $fallback--faint);
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
@ -154,5 +149,11 @@
|
|||
.fa-scale-110 {
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: 0.6rem;
|
||||
top: 1.25em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,6 +18,8 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
|||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -75,7 +75,8 @@ const PostStatusForm = {
|
|||
'autoFocus',
|
||||
'fileLimit',
|
||||
'submitOnEnter',
|
||||
'emojiPickerPlacement'
|
||||
'emojiPickerPlacement',
|
||||
'optimisticPosting'
|
||||
],
|
||||
components: {
|
||||
MediaUpload,
|
||||
|
@ -272,7 +273,7 @@ const PostStatusForm = {
|
|||
if (this.preview) this.previewStatus()
|
||||
},
|
||||
async postStatus (event, newStatus, opts = {}) {
|
||||
if (this.posting) { return }
|
||||
if (this.posting && !this.optimisticPosting) { return }
|
||||
if (this.disableSubmit) { return }
|
||||
if (this.emojiInputShown) { return }
|
||||
if (this.submitOnEnter) {
|
||||
|
@ -280,6 +281,8 @@ const PostStatusForm = {
|
|||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
|
||||
|
||||
if (this.emptyStatus) {
|
||||
this.error = this.$t('post_status.empty_status_error')
|
||||
return
|
||||
|
@ -528,7 +531,7 @@ const PostStatusForm = {
|
|||
!(isFormBiggerThanScroller &&
|
||||
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
|
||||
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
|
||||
const targetScroll = currentScroll + totalDelta
|
||||
const targetScroll = Math.round(currentScroll + totalDelta)
|
||||
|
||||
if (scrollerRef === window) {
|
||||
scrollerRef.scroll(0, targetScroll)
|
||||
|
|
|
@ -85,11 +85,16 @@
|
|||
{{ $t('post_status.preview') }}
|
||||
<FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" />
|
||||
</a>
|
||||
<FAIcon
|
||||
<div
|
||||
v-show="previewLoading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
class="preview-spinner"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-old-padding"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showPreview"
|
||||
|
@ -124,7 +129,7 @@
|
|||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
:disabled="posting"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
size="1"
|
||||
class="form-post-subject"
|
||||
>
|
||||
|
@ -150,7 +155,7 @@
|
|||
:placeholder="placeholder || $t('post_status.default')"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:disabled="posting"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
class="form-post-body"
|
||||
:class="{ 'scrollable-form': !!maxHeight }"
|
||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||
|
@ -383,12 +388,12 @@
|
|||
}
|
||||
|
||||
.preview-heading {
|
||||
padding-left: 0.5em;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.preview-toggle {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
class="react-button-popover"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
|
@ -12,6 +13,7 @@
|
|||
<div class="reaction-picker-filter">
|
||||
<input
|
||||
v-model="filterWord"
|
||||
size="1"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
>
|
||||
</div>
|
||||
|
@ -36,12 +38,13 @@
|
|||
<div class="reaction-bottom-fader" />
|
||||
</div>
|
||||
</div>
|
||||
<FAIcon
|
||||
slot="trigger"
|
||||
class="fa-scale-110 fa-old-padding add-reaction-button"
|
||||
:icon="['far', 'smile-beam']"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
/>
|
||||
<span slot="trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding add-reaction-button"
|
||||
:icon="['far', 'smile-beam']"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,46 +1,47 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="search-bar-container">
|
||||
<a
|
||||
v-if="hidden"
|
||||
href="#"
|
||||
class="nav-icon"
|
||||
:title="$t('nav.search')"
|
||||
><FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="search"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/></a>
|
||||
<template v-else>
|
||||
<input
|
||||
id="search-bar-input"
|
||||
ref="searchInput"
|
||||
v-model="searchTerm"
|
||||
class="search-bar-input"
|
||||
:placeholder="$t('nav.search')"
|
||||
type="text"
|
||||
@keyup.enter="find(searchTerm)"
|
||||
>
|
||||
<button
|
||||
class="btn search-button"
|
||||
@click="find(searchTerm)"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="search"
|
||||
/>
|
||||
</button>
|
||||
<span>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="times"
|
||||
class="cancel-icon fa-scale-110 fa-old-padding"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
class="SearchBar"
|
||||
:class="{ '-expanded': !hidden }"
|
||||
>
|
||||
<a
|
||||
v-if="hidden"
|
||||
href="#"
|
||||
class="nav-icon"
|
||||
:title="$t('nav.search')"
|
||||
><FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="search"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/></a>
|
||||
<template v-else>
|
||||
<input
|
||||
id="search-bar-input"
|
||||
ref="searchInput"
|
||||
v-model="searchTerm"
|
||||
class="search-bar-input"
|
||||
:placeholder="$t('nav.search')"
|
||||
type="text"
|
||||
@keyup.enter="find(searchTerm)"
|
||||
>
|
||||
<button
|
||||
class="btn search-button"
|
||||
@click="find(searchTerm)"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="search"
|
||||
/>
|
||||
</button>
|
||||
<span>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="times"
|
||||
class="cancel-icon fa-scale-110 fa-old-padding"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -49,21 +50,23 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.search-bar-container {
|
||||
max-width: 100%;
|
||||
.SearchBar {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
vertical-align: baseline;
|
||||
justify-content: flex-end;
|
||||
|
||||
&.-expanded {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar-input,
|
||||
.search-button {
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.search-bar-input {
|
||||
// TODO: do this properly without a rough guesstimate of 2 icons + paddings
|
||||
max-width: calc(100% - 30px - 30px - 20px);
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.cancel-icon {
|
||||
|
|
|
@ -126,6 +126,8 @@ library.add(
|
|||
faRetweet,
|
||||
faReply
|
||||
)
|
||||
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
/> {{ $t("nav.chats") }}
|
||||
<span
|
||||
v-if="unreadChatCount"
|
||||
class="badge badge-notification unread-chat-count"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ unreadChatCount }}
|
||||
</span>
|
||||
|
@ -99,7 +99,7 @@
|
|||
/> {{ $t("nav.friend_requests") }}
|
||||
<span
|
||||
v-if="followRequestCount > 0"
|
||||
class="badge follow-request-count"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ followRequestCount }}
|
||||
</span>
|
||||
|
@ -272,12 +272,11 @@
|
|||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.follow-request-count {
|
||||
vertical-align: baseline;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--input, $fallback--faint);
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: 0.7rem;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.side-drawer-logo-wrapper {
|
||||
|
|
|
@ -7,8 +7,9 @@ $status-margin: 0.75em;
|
|||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
--still-image-img: visible;
|
||||
--still-image-canvas: hidden;
|
||||
--_still-image-img-visibility: visible;
|
||||
--_still-image-canvas-visibility: hidden;
|
||||
--_still-image-label-visibility: hidden;
|
||||
}
|
||||
|
||||
&.-focused {
|
||||
|
@ -58,6 +59,15 @@ $status-margin: 0.75em;
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
._misclick-prevention & {
|
||||
pointer-events: none;
|
||||
|
||||
.attachments {
|
||||
pointer-events: initial;
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.left-side {
|
||||
margin-right: $status-margin;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
visibility: var(--still-image-canvas, visible);
|
||||
visibility: var(--_still-image-canvas-visibility, visible);
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -66,16 +66,19 @@
|
|||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
z-index: 2;
|
||||
visibility: var(--still-image-label-visibility, visible);
|
||||
visibility: var(--_still-image-label-visibility, visible);
|
||||
}
|
||||
|
||||
&:hover canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:hover::before {
|
||||
visibility: var(--_still-image-label-visibility, hidden);
|
||||
}
|
||||
|
||||
img {
|
||||
visibility: var(--still-image-img, hidden);
|
||||
visibility: var(--_still-image-img-visibility, hidden);
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
display: flex;
|
||||
|
||||
.tab-icon {
|
||||
width: 100%;
|
||||
margin: 0.2em 0;
|
||||
margin: 0.2em auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import Status from '../status/status.vue'
|
|||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import Conversation from '../conversation/conversation.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 { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
@ -40,7 +40,8 @@ const Timeline = {
|
|||
paused: false,
|
||||
unfocused: false,
|
||||
bottomedOut: false,
|
||||
virtualScrollIndex: 0
|
||||
virtualScrollIndex: 0,
|
||||
blockingClicks: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -70,8 +71,10 @@ const Timeline = {
|
|||
}
|
||||
},
|
||||
classes () {
|
||||
let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
|
||||
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
|
||||
return {
|
||||
root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []),
|
||||
root: rootClasses,
|
||||
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
|
||||
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
||||
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
||||
|
@ -130,6 +133,15 @@ const Timeline = {
|
|||
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
},
|
||||
methods: {
|
||||
stopBlockingClicks: debounce(function () {
|
||||
this.blockingClicks = false
|
||||
}, 1000),
|
||||
blockClicksTemporarily () {
|
||||
if (!this.blockingClicks) {
|
||||
this.blockingClicks = true
|
||||
}
|
||||
this.stopBlockingClicks()
|
||||
},
|
||||
handleShortKey (e) {
|
||||
// Ignore when input fields are focused
|
||||
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.fetchOlderStatuses()
|
||||
} else {
|
||||
this.blockClicksTemporarily()
|
||||
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
|
||||
this.paused = false
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="[classes.root, 'timeline']">
|
||||
<div :class="[classes.root, 'Timeline']">
|
||||
<div :class="classes.header">
|
||||
<TimelineMenu v-if="!embedded" />
|
||||
<div
|
||||
|
@ -107,10 +107,14 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.timeline {
|
||||
.Timeline {
|
||||
.loadmore-text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.-blocked {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-heading {
|
||||
|
|
|
@ -20,11 +20,14 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.Avatar {
|
||||
--still-image-label-visibility: hidden;
|
||||
--_avatarShadowBox: var(--avatarStatusShadow);
|
||||
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
||||
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
||||
--_still-image-label-visibility: hidden;
|
||||
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
box-shadow: var(--_avatarShadowBox);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
|
||||
|
@ -34,8 +37,8 @@
|
|||
}
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
box-shadow: var(--_avatarShadowInset);
|
||||
filter: var(--_avatarShadowFilter);
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
|
|
|
@ -282,6 +282,11 @@
|
|||
.user-card {
|
||||
position: relative;
|
||||
|
||||
&:hover .Avatar {
|
||||
--_still-image-img-visibility: visible;
|
||||
--_still-image-canvas-visibility: hidden;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
padding: .5em 0;
|
||||
text-align: center;
|
||||
|
@ -382,20 +387,17 @@
|
|||
max-height: 56px;
|
||||
|
||||
.Avatar {
|
||||
--_avatarShadowBox: var(--avatarShadow);
|
||||
--_avatarShadowFilter: var(--avatarShadowFilter);
|
||||
--_avatarShadowInset: var(--avatarShadowInset);
|
||||
|
||||
flex: 1 0 100%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
|
||||
box-shadow: var(--avatarShadow);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .Avatar {
|
||||
--still-image-img: visible;
|
||||
--still-image-canvas: hidden;
|
||||
}
|
||||
|
||||
&-avatar-link {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -20,14 +20,13 @@
|
|||
:key="index"
|
||||
class="user-profile-field"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<dt
|
||||
:title="user.fields_text[index].name"
|
||||
class="user-profile-field-name"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
{{ field.name }}
|
||||
</dt>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
v-html="field.name"
|
||||
/>
|
||||
<dd
|
||||
:title="user.fields_text[index].value"
|
||||
class="user-profile-field-value"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"features_panel": {
|
||||
"chat": "Babilejo",
|
||||
"gopher": "Gopher",
|
||||
"media_proxy": "Vidaŭdaĵa prokurilo",
|
||||
"media_proxy": "Vidaŭdaĵa retperilo",
|
||||
"scope_options": "Agordoj de amplekso",
|
||||
"text_limit": "Limo de teksto",
|
||||
"title": "Funkcioj",
|
||||
|
@ -33,7 +33,8 @@
|
|||
"show_more": "Montri plion",
|
||||
"retry": "Reprovi",
|
||||
"error_retry": "Bonvolu reprovi",
|
||||
"loading": "Enlegante…"
|
||||
"loading": "Enlegante…",
|
||||
"peek": "Antaŭmontri"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Tondi bildon",
|
||||
|
@ -70,9 +71,9 @@
|
|||
"friend_requests": "Petoj pri abono",
|
||||
"mentions": "Mencioj",
|
||||
"dms": "Rektaj mesaĝoj",
|
||||
"public_tl": "Publika tempolinio",
|
||||
"timeline": "Tempolinio",
|
||||
"twkn": "La tuta konata reto",
|
||||
"public_tl": "Publika historio",
|
||||
"timeline": "Historio",
|
||||
"twkn": "Konata reto",
|
||||
"user_search": "Serĉi uzantojn",
|
||||
"who_to_follow": "Kiun aboni",
|
||||
"preferences": "Agordoj",
|
||||
|
@ -80,7 +81,8 @@
|
|||
"search": "Serĉi",
|
||||
"interactions": "Interagoj",
|
||||
"administration": "Administrado",
|
||||
"bookmarks": "Legosignoj"
|
||||
"bookmarks": "Legosignoj",
|
||||
"timelines": "Historioj"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Nekonata stato, serĉante ĝin…",
|
||||
|
@ -107,14 +109,14 @@
|
|||
"text/html": "HTML"
|
||||
},
|
||||
"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.",
|
||||
"posting": "Afiŝante",
|
||||
"scope": {
|
||||
"direct": "Rekta – Afiŝi nur al menciitaj uzantoj",
|
||||
"private": "Nur abonantoj – Afiŝi nur al abonantoj",
|
||||
"public": "Publika – Afiŝi al publikaj tempolinioj",
|
||||
"unlisted": "Nelistigita – Ne afiŝi al publikaj tempolinioj"
|
||||
"public": "Publika – Afiŝi al publikaj historioj",
|
||||
"unlisted": "Nelistigita – Ne afiŝi al publikaj historioj"
|
||||
},
|
||||
"scope_notice": {
|
||||
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
|
||||
|
@ -193,7 +195,7 @@
|
|||
"foreground": "Malfono",
|
||||
"general": "Ĝenerala",
|
||||
"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",
|
||||
"hide_isp": "Kaŝi breton propran al nodo",
|
||||
"preload_images": "Antaŭ-enlegi bildojn",
|
||||
|
@ -246,7 +248,7 @@
|
|||
"profile_banner": "Rubando de profilo",
|
||||
"profile_tab": "Profilo",
|
||||
"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_following": "Montri nur respondojn por mi aŭ miaj abonatoj",
|
||||
"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.",
|
||||
"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.",
|
||||
"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_snapshot": "Malnova versio",
|
||||
|
@ -352,10 +359,11 @@
|
|||
"icons": "Bildsimboloj",
|
||||
"poll": "Grafo de enketo",
|
||||
"underlay": "Subtavolo",
|
||||
"popover": "Ŝpruchelpiloj, menuoj",
|
||||
"popover": "Ŝprucaĵoj, menuoj",
|
||||
"post": "Afiŝoj/Priskriboj de uzantoj",
|
||||
"alert_neutral": "Neŭtrala",
|
||||
"alert_warning": "Averto"
|
||||
"alert_warning": "Averto",
|
||||
"toggled": "Ŝaltita"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Rondeco"
|
||||
|
@ -388,7 +396,8 @@
|
|||
"buttonPressed": "Butono (premita)",
|
||||
"buttonPressedHover": "Butono (premita kaj je ŝvebo)",
|
||||
"input": "Eniga kampo"
|
||||
}
|
||||
},
|
||||
"hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}."
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "Tiparoj",
|
||||
|
@ -411,7 +420,7 @@
|
|||
"button": "Butono",
|
||||
"text": "Kelko da pliaj {0} kaj {1}",
|
||||
"mono": "enhavo",
|
||||
"input": "Ĵus alvenis al la Universala Kongreso!",
|
||||
"input": "Ĵus alvenis Esperantujon!",
|
||||
"faint_link": "helpan manlibron",
|
||||
"fine_print": "Legu nian {0} por nenion utilan ekscii!",
|
||||
"header_faint": "Tio estas en ordo",
|
||||
|
@ -420,7 +429,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!",
|
||||
"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.",
|
||||
"generate_new_recovery_codes": "Estigi novajn rehavajn kodojn",
|
||||
"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",
|
||||
"security": "Sekureco",
|
||||
|
@ -480,11 +492,11 @@
|
|||
},
|
||||
"import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero",
|
||||
"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",
|
||||
"domain_mutes": "Retnomoj",
|
||||
"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_privacy": "Privateco",
|
||||
"notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas",
|
||||
|
@ -495,7 +507,14 @@
|
|||
"backend_version": "Versio de internaĵo",
|
||||
"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": {
|
||||
"collapse": "Maletendi",
|
||||
|
@ -503,7 +522,7 @@
|
|||
"error_fetching": "Eraris ĝisdatigo",
|
||||
"load_older": "Montri pli malnovajn statojn",
|
||||
"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",
|
||||
"up_to_date": "Ĝisdata",
|
||||
"no_more_statuses": "Neniuj pliaj statoj",
|
||||
|
@ -648,21 +667,22 @@
|
|||
"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": "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": "Kvaranteno",
|
||||
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
|
||||
"reject": "Rifuzi",
|
||||
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
|
||||
"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)",
|
||||
"keyword": {
|
||||
"is_replaced_by": "→",
|
||||
"replace": "Anstataŭigi",
|
||||
"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"
|
||||
},
|
||||
"federation": "Federado",
|
||||
|
@ -707,7 +727,8 @@
|
|||
"pin": "Fiksi al profilo",
|
||||
"delete": "Forigi staton",
|
||||
"repeats": "Ripetoj",
|
||||
"favorites": "Ŝatataj"
|
||||
"favorites": "Ŝatoj",
|
||||
"status_deleted": "Ĉi tiu afiŝo foriĝis"
|
||||
},
|
||||
"time": {
|
||||
"years_short": "{0}j",
|
||||
|
@ -769,7 +790,8 @@
|
|||
"new": "Nova babilo",
|
||||
"chats": "Babiloj",
|
||||
"delete": "Forigi",
|
||||
"you": "Vi:"
|
||||
"you": "Vi:",
|
||||
"message_user": "Mesaĝi al {nickname}"
|
||||
},
|
||||
"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.",
|
||||
|
@ -791,5 +813,8 @@
|
|||
"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:",
|
||||
"title": "Raportante {0}"
|
||||
},
|
||||
"shoutbox": {
|
||||
"title": "Kriujo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
"repeated_you": "转发了你的状态",
|
||||
"no_more_notifications": "没有更多的通知",
|
||||
"reacted_with": "作出了 {0} 的反应",
|
||||
"migrated_to": "迁移到",
|
||||
"migrated_to": "迁移到了",
|
||||
"follow_request": "想要关注你"
|
||||
},
|
||||
"polls": {
|
||||
|
@ -165,7 +165,7 @@
|
|||
"registration": {
|
||||
"bio": "简介",
|
||||
"email": "电子邮箱",
|
||||
"fullname": "全名",
|
||||
"fullname": "显示名称",
|
||||
"password_confirm": "确认密码",
|
||||
"registration": "注册",
|
||||
"token": "邀请码",
|
||||
|
@ -322,7 +322,7 @@
|
|||
"search_user_to_mute": "搜索你想要隐藏的用户",
|
||||
"security_tab": "安全",
|
||||
"scope_copy": "回复时的复制范围(私信是总是复制的)",
|
||||
"minimal_scopes_mode": "最小发文范围",
|
||||
"minimal_scopes_mode": "使发文可见范围的选项最少化",
|
||||
"set_new_avatar": "设置新头像",
|
||||
"set_new_profile_background": "设置新的个人资料背景",
|
||||
"set_new_profile_banner": "设置新的横幅图片",
|
||||
|
|
|
@ -680,7 +680,7 @@
|
|||
"fullname": "顯示名稱",
|
||||
"bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。",
|
||||
"fullname_placeholder": "例如:岩倉玲音",
|
||||
"username_placeholder": "例如:玲音",
|
||||
"username_placeholder": "例如:lain",
|
||||
"new_captcha": "點擊圖片獲取新的驗證碼",
|
||||
"captcha": "CAPTCHA",
|
||||
"token": "邀請碼",
|
||||
|
|
|
@ -75,12 +75,18 @@ const api = {
|
|||
} else if (message.event === 'delete') {
|
||||
dispatch('deleteStatusById', message.id)
|
||||
} else if (message.event === 'pleroma:chat_update') {
|
||||
dispatch('addChatMessages', {
|
||||
chatId: message.chatUpdate.id,
|
||||
messages: [message.chatUpdate.lastMessage]
|
||||
})
|
||||
dispatch('updateChat', { chat: message.chatUpdate })
|
||||
maybeShowChatNotification(store, message.chatUpdate)
|
||||
// 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', {
|
||||
chatId: message.chatUpdate.id,
|
||||
messages: [message.chatUpdate.lastMessage]
|
||||
})
|
||||
dispatch('updateChat', { chat: message.chatUpdate })
|
||||
maybeShowChatNotification(store, message.chatUpdate)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -16,7 +16,8 @@ const defaultState = {
|
|||
openedChats: {},
|
||||
openedChatMessageServices: {},
|
||||
fetcher: undefined,
|
||||
currentChatId: null
|
||||
currentChatId: null,
|
||||
lastReadMessageId: null
|
||||
}
|
||||
|
||||
const getChatById = (state, id) => {
|
||||
|
@ -92,9 +93,14 @@ const chats = {
|
|||
commit('setCurrentChatFetcher', { fetcher: undefined })
|
||||
},
|
||||
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
||||
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
|
||||
|
||||
dispatch('resetChatNewMessageCount')
|
||||
commit('readChat', { id })
|
||||
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||
commit('readChat', { id, lastReadId })
|
||||
|
||||
if (isNewMessage) {
|
||||
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||
}
|
||||
},
|
||||
deleteChatMessage ({ rootState, commit }, value) {
|
||||
rootState.api.backendInteractor.deleteChatMessage(value)
|
||||
|
@ -106,6 +112,9 @@ const chats = {
|
|||
},
|
||||
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
||||
commit('clearOpenedChats', { commit })
|
||||
},
|
||||
handleMessageError ({ commit }, value) {
|
||||
commit('handleMessageError', { commit, ...value })
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
|
@ -208,11 +217,16 @@ const chats = {
|
|||
}
|
||||
}
|
||||
},
|
||||
readChat (state, { id }) {
|
||||
readChat (state, { id, lastReadId }) {
|
||||
state.lastReadMessageId = lastReadId
|
||||
const chat = getChatById(state, id)
|
||||
if (chat) {
|
||||
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,
|
||||
hideUserStats: false,
|
||||
loginMethod: 'password',
|
||||
logo: '/static/logo.png',
|
||||
logo: '/static/logo.svg',
|
||||
logoMargin: '.2em',
|
||||
logoMask: true,
|
||||
logoLeft: false,
|
||||
minimalScopesMode: false,
|
||||
nsfwCensorImage: undefined,
|
||||
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 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 = {
|
||||
'content': content
|
||||
}
|
||||
|
@ -1234,11 +1238,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
|||
payload['media_id'] = mediaId
|
||||
}
|
||||
|
||||
const headers = {}
|
||||
|
||||
if (idempotencyKey) {
|
||||
headers['idempotency-key'] = idempotencyKey
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||
method: 'POST',
|
||||
payload: payload,
|
||||
credentials
|
||||
credentials,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ import _ from 'lodash'
|
|||
const empty = (chatId) => {
|
||||
return {
|
||||
idIndex: {},
|
||||
idempotencyKeyIndex: {},
|
||||
messages: [],
|
||||
newMessageCount: 0,
|
||||
lastSeenTimestamp: 0,
|
||||
lastSeenMessageId: '0',
|
||||
chatId: chatId,
|
||||
minId: undefined,
|
||||
maxId: undefined
|
||||
|
@ -13,10 +14,20 @@ const empty = (chatId) => {
|
|||
}
|
||||
|
||||
const clear = (storage) => {
|
||||
storage.idIndex = {}
|
||||
storage.messages.splice(0, storage.messages.length)
|
||||
const failedMessageIds = []
|
||||
|
||||
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.lastSeenTimestamp = 0
|
||||
storage.lastSeenMessageId = '0'
|
||||
storage.minId = 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 }) => {
|
||||
if (!storage) { return }
|
||||
for (let i = 0; i < newMessages.length; i++) {
|
||||
|
@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
|||
// sanity check
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!storage.idIndex[message.id]) {
|
||||
if (storage.lastSeenTimestamp < message.created_at) {
|
||||
if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
|
||||
if (storage.lastSeenMessageId < message.id) {
|
||||
storage.newMessageCount++
|
||||
}
|
||||
storage.messages.push(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) => {
|
||||
if (!storage) { return }
|
||||
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
|
||||
|
@ -76,7 +130,7 @@ const getView = (storage) => {
|
|||
if (!storage) { return [] }
|
||||
|
||||
const result = []
|
||||
const messages = _.sortBy(storage.messages, ['id', 'desc'])
|
||||
const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
|
||||
const firstMessage = messages[0]
|
||||
let previousMessage = messages[messages.length - 1]
|
||||
let currentMessageChainId
|
||||
|
@ -148,7 +202,8 @@ const ChatService = {
|
|||
getView,
|
||||
deleteMessage,
|
||||
resetNewMessageCount,
|
||||
clear
|
||||
clear,
|
||||
handleMessageError
|
||||
}
|
||||
|
||||
export default ChatService
|
||||
|
|
|
@ -3,7 +3,7 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
|
|||
export const maybeShowChatNotification = (store, chat) => {
|
||||
if (!chat.lastMessage) 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 = {
|
||||
tag: chat.lastMessage.id,
|
||||
|
@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => {
|
|||
|
||||
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_html = data.fields.map(field => {
|
||||
return {
|
||||
name: addEmojis(field.name, data.emojis),
|
||||
name: addEmojis(escape(field.name), data.emojis),
|
||||
value: addEmojis(field.value, data.emojis)
|
||||
}
|
||||
})
|
||||
|
@ -429,6 +429,9 @@ export const parseChatMessage = (message) => {
|
|||
} else {
|
||||
output.attachments = []
|
||||
}
|
||||
output.pending = !!message.pending
|
||||
output.error = false
|
||||
output.idempotency_key = message.idempotency_key
|
||||
output.isNormalized = true
|
||||
return output
|
||||
}
|
||||
|
|
|
@ -10,7 +10,14 @@ export const promiseInterval = (promiseCall, interval) => {
|
|||
let timeout = null
|
||||
|
||||
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
|
||||
timeout = window.setTimeout(func, interval)
|
||||
})
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
"hideSitename": false,
|
||||
"hideUserStats": false,
|
||||
"loginMethod": "password",
|
||||
"logo": "/static/logo.png",
|
||||
"logo": "/static/logo.svg",
|
||||
"logoMargin": ".1em",
|
||||
"logoMask": true,
|
||||
"logoLeft": false,
|
||||
"minimalScopesMode": false,
|
||||
"nsfwCensorImage": "",
|
||||
"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>
|
||||
|
||||
<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 = {
|
||||
id: '9wLkdcmQXD21Oy8lEX',
|
||||
idempotency_key: '1',
|
||||
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
||||
}
|
||||
|
||||
const message2 = {
|
||||
id: '9wLkdp6ihaOVdNj8Wu',
|
||||
idempotency_key: '2',
|
||||
account_id: '9vmRb29zLQReckr5ay',
|
||||
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
||||
}
|
||||
|
||||
const message3 = {
|
||||
id: '9wLke9zL4Dy4OZR2RM',
|
||||
idempotency_key: '3',
|
||||
account_id: '9vmRb29zLQReckr5ay',
|
||||
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
||||
}
|
||||
|
@ -44,10 +47,10 @@ describe('chatService', () => {
|
|||
|
||||
chatService.resetNewMessageCount(chat)
|
||||
expect(chat.newMessageCount).to.eql(0)
|
||||
expect(chat.lastSeenMessageId).to.eql(message2.id)
|
||||
|
||||
const createdAt = new Date()
|
||||
createdAt.setSeconds(createdAt.getSeconds() + 10)
|
||||
chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] })
|
||||
// Add message with higher id
|
||||
chatService.add(chat, { messages: [ message3 ] })
|
||||
expect(chat.newMessageCount).to.eql(1)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue