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

This commit is contained in:
sadposter 2021-03-23 23:01:44 +00:00
commit c7c4e2e03e
40 changed files with 585 additions and 214 deletions

View file

@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Added a quick settings to timeline header for easier access - Added a quick settings to timeline header for easier access
- Added option to mark posts as sensitive by default - Added option to mark posts as sensitive by default
- Added quick filters for notifications
## [2.3.0] - 2021-03-01 ## [2.3.0] - 2021-03-01
### Fixed ### Fixed
@ -20,6 +22,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Display 'people voted' instead of 'votes' for multi-choice polls - Display 'people voted' instead of 'votes' for multi-choice polls
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
- Renamed "Timeline" to "Home Timeline" to be more clear
- Optimized chat to not get horrible performance after keeping the same chat open for a long time - Optimized chat to not get horrible performance after keeping the same chat open for a long time
- When opening emoji picker or react picker, it automatically focuses the search field - When opening emoji picker or react picker, it automatically focuses the search field
- Language picker now uses native language names - Language picker now uses native language names
@ -28,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added reason field for registration when approval is required - Added reason field for registration when approval is required
- Group staff members by role in the About page - Group staff members by role in the About page
## [2.2.3] - 2021-01-18 ## [2.2.3] - 2021-01-18
### Added ### Added
- Added Report button to status ellipsis menu for easier reporting - Added Report button to status ellipsis menu for easier reporting
@ -35,6 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Follows/Followers tabs on user profiles now display the content properly. - Follows/Followers tabs on user profiles now display the content properly.
- Handle punycode in screen names - Handle punycode in screen names
- Fixed local dev mode having non-functional websockets in some cases
- Show notices for websocket events (errors, abnormal closures, reconnections)
- Fix not being able to re-enable websocket until page refresh
- Fix annoying issue where timeline might have few posts when streaming is enabled
### Changed ### Changed
- Don't filter own posts when they hit your wordfilter - Don't filter own posts when they hit your wordfilter

View file

@ -3,6 +3,11 @@ const path = require('path')
let settings = {} let settings = {}
try { try {
settings = require('./local.json') settings = require('./local.json')
if (settings.target && settings.target.endsWith('/')) {
// replacing trailing slash since it can conflict with some apis
// and that's how actual BE reports its url
settings.target = settings.target.replace(/\/$/, '')
}
console.log('Using local dev server settings (/config/local.json):') console.log('Using local dev server settings (/config/local.json):')
console.log(JSON.stringify(settings, null, 2)) console.log(JSON.stringify(settings, null, 2))
} catch (e) { } catch (e) {

View file

@ -34,7 +34,6 @@
"punycode.js": "^2.1.0", "punycode.js": "^2.1.0",
"v-click-outside": "^2.1.1", "v-click-outside": "^2.1.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",

View file

@ -706,6 +706,15 @@ nav {
color: var(--alertWarningPanelText, $fallback--text); color: var(--alertWarningPanelText, $fallback--text);
} }
} }
&.success {
background-color: var(--alertSuccess, $fallback--alertWarning);
color: var(--alertSuccessText, $fallback--text);
.panel-heading & {
color: var(--alertSuccessPanelText, $fallback--text);
}
}
} }
.faint { .faint {

View file

@ -35,6 +35,18 @@ const chatPanel = {
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames) return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
} }
},
watch: {
messages (newVal) {
const scrollEl = this.$el.querySelector('.chat-window')
if (!scrollEl) return
if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) {
this.$nextTick(() => {
if (!scrollEl) return
scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight
})
}
}
} }
} }

View file

@ -10,17 +10,15 @@
@click.stop.prevent="togglePanel" @click.stop.prevent="togglePanel"
> >
<div class="title"> <div class="title">
<span>{{ $t('shoutbox.title') }}</span> {{ $t('shoutbox.title') }}
<FAIcon <FAIcon
v-if="floating" v-if="floating"
icon="times" icon="times"
class="close-icon"
/> />
</div> </div>
</div> </div>
<div <div class="chat-window">
v-chat-scroll
class="chat-window"
>
<div <div
v-for="message in messages" v-for="message in messages"
:key="message.id" :key="message.id"
@ -94,6 +92,13 @@
.icon { .icon {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
margin-right: 0.5em;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
} }
} }

View file

@ -71,6 +71,14 @@
} }
} }
.global-success {
background-color: var(--alertPopupSuccess, $fallback--cGreen);
color: var(--alertPopupSuccessText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupSuccessText, $fallback--text);
}
}
.global-info { .global-info {
background-color: var(--alertPopupNeutral, $fallback--fg); background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text); color: var(--alertPopupNeutralText, $fallback--text);

View file

@ -1,6 +1,11 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
import DialogModal from '../dialog_modal/dialog_modal.vue' import DialogModal from '../dialog_modal/dialog_modal.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
library.add(faChevronDown)
const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip' const STRIP_MEDIA = 'mrf_tag:media-strip'
const FORCE_UNLISTED = 'mrf_tag:force-unlisted' const FORCE_UNLISTED = 'mrf_tag:force-unlisted'

View file

@ -124,10 +124,11 @@
</div> </div>
<button <button
slot="trigger" slot="trigger"
class="btn button-default btn-block" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled }"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" />
</button> </button>
</Popover> </Popover>
<portal to="modal"> <portal to="modal">
@ -170,4 +171,10 @@
height: 100%; height: 100%;
} }
} }
.moderation-tools-button {
svg,i {
font-size: 0.8em;
}
}
</style> </style>

View file

@ -1,4 +1,4 @@
import { timelineNames } from '../timeline_menu/timeline_menu.js' import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -7,10 +7,12 @@ import {
faGlobe, faGlobe,
faBookmark, faBookmark,
faEnvelope, faEnvelope,
faHome, faChevronDown,
faChevronUp,
faComments, faComments,
faBell, faBell,
faInfoCircle faInfoCircle,
faStream
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -18,10 +20,12 @@ library.add(
faGlobe, faGlobe,
faBookmark, faBookmark,
faEnvelope, faEnvelope,
faHome, faChevronDown,
faChevronUp,
faComments, faComments,
faBell, faBell,
faInfoCircle faInfoCircle,
faStream
) )
const NavPanel = { const NavPanel = {
@ -30,16 +34,20 @@ const NavPanel = {
this.$store.dispatch('startFetchingFollowRequests') this.$store.dispatch('startFetchingFollowRequests')
} }
}, },
components: {
TimelineMenuContent
},
data () {
return {
showTimelines: false
}
},
methods: {
toggleTimelines () {
this.showTimelines = !this.showTimelines
}
},
computed: { computed: {
onTimelineRoute () {
return !!timelineNames()[this.$route.name]
},
timelinesRoute () {
if (this.$store.state.interface.lastTimeline) {
return this.$store.state.interface.lastTimeline
}
return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length, followRequestCount: state => state.api.followRequests.length,

View file

@ -3,19 +3,33 @@
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if="currentUser || !privateMode"> <li v-if="currentUser || !privateMode">
<router-link <button
:to="{ name: timelinesRoute }" class="button-unstyled menu-item"
:class="onTimelineRoute && 'router-link-active'" @click="toggleTimelines"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
icon="home" icon="stream"
/>{{ $t("nav.timelines") }} />{{ $t("nav.timelines") }}
</router-link> <FAIcon
class="timelines-chevron"
fixed-width
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
/>
</button>
<div
v-show="showTimelines"
class="timelines-background"
>
<TimelineMenuContent class="timelines" />
</div>
</li> </li>
<li v-if="currentUser"> <li v-if="currentUser">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> <router-link
class="menu-item"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@ -24,7 +38,10 @@
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && pleromaChatMessagesAvailable"> <li v-if="currentUser && pleromaChatMessagesAvailable">
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> <router-link
class="menu-item"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<div <div
v-if="unreadChatCount" v-if="unreadChatCount"
class="badge badge-notification" class="badge badge-notification"
@ -39,7 +56,10 @@
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && currentUser.locked"> <li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }"> <router-link
class="menu-item"
:to="{ name: 'friend-requests' }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@ -54,7 +74,10 @@
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: 'about' }"> <router-link
class="menu-item"
:to="{ name: 'about' }"
>
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110" class="fa-scale-110"
@ -91,14 +114,14 @@
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
padding: 0; padding: 0;
&:first-child a { &:first-child .menu-item {
border-top-right-radius: $fallback--panelRadius; border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius; border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius); border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
} }
&:last-child a { &:last-child .menu-item {
border-bottom-right-radius: $fallback--panelRadius; border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius; border-bottom-left-radius: $fallback--panelRadius;
@ -110,13 +133,15 @@
border: none; border: none;
} }
a { .menu-item {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
align-items: stretch;
height: 3.5em; height: 3.5em;
line-height: 3.5em; line-height: 3.5em;
padding: 0 1em; padding: 0 1em;
width: 100%;
color: $fallback--link;
color: var(--link, $fallback--link);
&:hover { &:hover {
background-color: $fallback--lightBg; background-color: $fallback--lightBg;
@ -146,6 +171,25 @@
} }
} }
.timelines-chevron {
margin-left: 0.8em;
font-size: 1.1em;
}
.timelines-background {
padding: 0 0 0 0.6em;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.timelines {
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.fa-scale-110 { .fa-scale-110 {
margin-right: 0.8em; margin-right: 0.8em;
} }

View file

@ -0,0 +1,122 @@
<template>
<Popover
trigger="click"
class="NotificationFilters"
placement="bottom"
:bound-to="{ x: 'container' }"
>
<template
v-slot:content
>
<div class="dropdown-menu">
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('likes')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.likes }"
/>{{ $t('settings.notification_visibility_likes') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('repeats')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.repeats }"
/>{{ $t('settings.notification_visibility_repeats') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('follows')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.follows }"
/>{{ $t('settings.notification_visibility_follows') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('mentions')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.mentions }"
/>{{ $t('settings.notification_visibility_mentions') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('emojiReactions')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.emojiReactions }"
/>{{ $t('settings.notification_visibility_emoji_reactions') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleNotificationFilter('moves')"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.moves }"
/>{{ $t('settings.notification_visibility_moves') }}
</button>
</div>
</template>
<template v-slot:trigger>
<FAIcon icon="filter" />
</template>
</Popover>
</template>
<script>
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
library.add(
faFilter
)
export default {
components: { Popover },
computed: {
filters () {
return this.$store.getters.mergedConfig.notificationVisibility
}
},
methods: {
toggleNotificationFilter (type) {
this.$store.dispatch('setOption', {
name: 'notificationVisibility',
value: {
...this.filters,
[type]: !this.filters[type]
}
})
}
}
}
</script>
<style lang="scss">
.NotificationFilters {
align-self: stretch;
> button {
font-size: 1.2em;
padding-left: 0.7em;
padding-right: 0.2em;
line-height: 100%;
height: 100%;
}
.dropdown-item {
margin: 0;
}
}
</style>

View file

@ -1,5 +1,6 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Notification from '../notification/notification.vue' import Notification from '../notification/notification.vue'
import NotificationFilters from './notification_filters.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import { import {
notificationsFromStore, notificationsFromStore,
@ -17,6 +18,10 @@ library.add(
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = { const Notifications = {
components: {
Notification,
NotificationFilters
},
props: { props: {
// Disables display of panel header // Disables display of panel header
noHeading: Boolean, noHeading: Boolean,
@ -35,11 +40,6 @@ const Notifications = {
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
} }
}, },
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.fetchAndUpdate({ store, credentials })
},
computed: { computed: {
mainClass () { mainClass () {
return this.minimalMode ? '' : 'panel panel-default' return this.minimalMode ? '' : 'panel panel-default'
@ -70,9 +70,6 @@ const Notifications = {
}, },
...mapGetters(['unreadChatCount']) ...mapGetters(['unreadChatCount'])
}, },
components: {
Notification
},
watch: { watch: {
unseenCountTitle (count) { unseenCountTitle (count) {
if (count > 0) { if (count > 0) {

View file

@ -22,6 +22,7 @@
> >
{{ $t('notifications.read') }} {{ $t('notifications.read') }}
</button> </button>
<NotificationFilters />
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div <div

View file

@ -53,7 +53,7 @@
type="submit" type="submit"
class="btn button-default btn-block" class="btn button-default btn-block"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -56,6 +56,9 @@ const Popover = {
// Popover will be anchored around this element, trigger ref is the container, so // Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one slot="trigger". // its children are what are inside the slot. Expect only one slot="trigger".
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const screenBox = anchorEl.getBoundingClientRect() const screenBox = anchorEl.getBoundingClientRect()
// Screen position of the origin point for popover // Screen position of the origin point for popover
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top } const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
@ -114,11 +117,11 @@ const Popover = {
const yOffset = (this.offset && this.offset.y) || 0 const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop const translateY = usingTop
? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight ? -anchorHeight + vPadding - yOffset - content.offsetHeight
: yOffset : yOffset
const xOffset = (this.offset && this.offset.x) || 0 const xOffset = (this.offset && this.offset.x) || 0
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
// Note, separate translateX and translateY avoids blurry text on chromium, // Note, separate translateX and translateY avoids blurry text on chromium,
// single translate or translate3d resulted in blurry text. // single translate or translate3d resulted in blurry text.

View file

@ -272,7 +272,7 @@
disabled disabled
class="btn button-default" class="btn button-default"
> >
{{ $t('general.submit') }} {{ $t('post_status.post') }}
</button> </button>
<!-- touchstart is used to keep the OSK at the same position after a message send --> <!-- touchstart is used to keep the OSK at the same position after a message send -->
<button <button
@ -282,7 +282,7 @@
@touchstart.stop.prevent="postStatus($event, newStatus)" @touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)"
> >
{{ $t('general.submit') }} {{ $t('post_status.post') }}
</button> </button>
</div> </div>
<div <div

View file

@ -230,7 +230,7 @@
type="submit" type="submit"
class="btn button-default" class="btn button-default"
> >
{{ $t('general.submit') }} {{ $t('registration.register') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -24,7 +24,7 @@
class="btn button-default" class="btn button-default"
@click="updateNotificationSettings" @click="updateNotificationSettings"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -153,7 +153,7 @@
class="btn button-default" class="btn button-default"
@click="updateProfile" @click="updateProfile"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@ -227,7 +227,7 @@
class="btn button-default" class="btn button-default"
@click="submitBanner(banner)" @click="submitBanner(banner)"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@ -266,7 +266,7 @@
class="btn button-default" class="btn button-default"
@click="submitBackground(background)" @click="submitBackground(background)"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -22,7 +22,7 @@
class="btn button-default" class="btn button-default"
@click="changeEmail" @click="changeEmail"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
<p v-if="changedEmail"> <p v-if="changedEmail">
{{ $t('settings.changed_email') }} {{ $t('settings.changed_email') }}
@ -60,7 +60,7 @@
class="btn button-default" class="btn button-default"
@click="changePassword" @click="changePassword"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
<p v-if="changedPassword"> <p v-if="changedPassword">
{{ $t('settings.changed_password') }} {{ $t('settings.changed_password') }}
@ -133,7 +133,7 @@
class="btn button-default" class="btn button-default"
@click="confirmDelete" @click="confirmDelete"
> >
{{ $t('general.submit') }} {{ $t('settings.save') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import BooleanSetting from '../settings_modal/helpers/boolean_setting.vue'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons'
@ -12,8 +11,7 @@ library.add(
const TimelineQuickSettings = { const TimelineQuickSettings = {
components: { components: {
Popover, Popover
BooleanSetting
}, },
methods: { methods: {
setReplyVisibility (visibility) { setReplyVisibility (visibility) {

View file

@ -6,7 +6,7 @@
> >
<div <div
slot="content" slot="content"
class="timeline-settings-menu dropdown-menu" class="dropdown-menu"
> >
<div v-if="loggedIn"> <div v-if="loggedIn">
<button <button
@ -96,12 +96,6 @@
.dropdown-item { .dropdown-item {
margin: 0; margin: 0;
} }
.timeline-settings-menu {
display: flex;
min-width: 12em;
flex-direction: column;
}
} }
</style> </style>

View file

@ -1,29 +1,17 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import { mapState } from 'vuex' import TimelineMenuContent from './timeline_menu_content.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown faChevronDown
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faChevronDown)
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown
)
// Route -> i18n key mapping, exported and not in the computed // Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information. // because nav panel benefits from the same information.
export const timelineNames = () => { export const timelineNames = () => {
return { return {
'friends': 'nav.timeline', 'friends': 'nav.home_timeline',
'bookmarks': 'nav.bookmarks', 'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms', 'dms': 'nav.dms',
'public-timeline': 'nav.public_tl', 'public-timeline': 'nav.public_tl',
@ -33,7 +21,8 @@ export const timelineNames = () => {
const TimelineMenu = { const TimelineMenu = {
components: { components: {
Popover Popover,
TimelineMenuContent
}, },
data () { data () {
return { return {
@ -41,9 +30,6 @@ const TimelineMenu = {
} }
}, },
created () { created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
if (timelineNames()[this.$route.name]) { if (timelineNames()[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name) this.$store.dispatch('setLastTimeline', this.$route.name)
} }
@ -75,13 +61,6 @@ const TimelineMenu = {
const i18nkey = timelineNames()[this.$route.name] const i18nkey = timelineNames()[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route return i18nkey ? this.$t(i18nkey) : route
} }
},
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
} }
} }

View file

@ -13,53 +13,7 @@
slot="content" slot="content"
class="timeline-menu-popover panel panel-default" class="timeline-menu-popover panel panel-default"
> >
<ul> <TimelineMenuContent />
<li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
/>{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'bookmarks'}">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="bookmark"
/>{{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>{{ $t("nav.dms") }}
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="users"
/>{{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="globe"
/>{{ $t("nav.twkn") }}
</router-link>
</li>
</ul>
</div> </div>
<div <div
slot="trigger" slot="trigger"

View file

@ -0,0 +1,29 @@
import { mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome
)
const TimelineMenuContent = {
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
}
export default TimelineMenuContent

View file

@ -0,0 +1,66 @@
<template>
<ul>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'friends' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
/>{{ $t("nav.home_timeline") }}
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link
class="menu-item"
:to="{ name: 'public-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="users"
/>{{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link
class="menu-item"
:to="{ name: 'public-external-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="globe"
/>{{ $t("nav.twkn") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'bookmarks'}"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="bookmark"
/>{{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>{{ $t("nav.dms") }}
</router-link>
</li>
</ul>
</template>
<script src="./timeline_menu_content.js" ></script>

View file

@ -141,10 +141,10 @@
v-model="userHighlightType" v-model="userHighlightType"
class="userHighlightSel" class="userHighlightSel"
> >
<option value="disabled">No highlight</option> <option value="disabled">{{ $t('user_card.highlight.disabled') }}</option>
<option value="solid">Solid bg</option> <option value="solid">{{ $t('user_card.highlight.solid') }}</option>
<option value="striped">Striped bg</option> <option value="striped">{{ $t('user_card.highlight.striped') }}</option>
<option value="side">Side stripe</option> <option value="side">{{ $t('user_card.highlight.side') }}</option>
</select> </select>
<FAIcon <FAIcon
class="select-down-icon" class="select-down-icon"

View file

@ -3,27 +3,27 @@
"mrf": { "mrf": {
"federation": "Federation", "federation": "Federation",
"keyword": { "keyword": {
"keyword_policies": "Keyword Policies", "keyword_policies": "Keyword policies",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline", "ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"reject": "Reject", "reject": "Reject",
"replace": "Replace", "replace": "Replace",
"is_replaced_by": "→" "is_replaced_by": "→"
}, },
"mrf_policies": "Enabled MRF Policies", "mrf_policies": "Enabled MRF policies",
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:", "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"simple": { "simple": {
"simple_policies": "Instance-specific Policies", "simple_policies": "Instance-specific policies",
"accept": "Accept", "accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:", "accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject", "reject": "Reject",
"reject_desc": "This instance will not accept messages from the following instances:", "reject_desc": "This instance will not accept messages from the following instances:",
"quarantine": "Quarantine", "quarantine": "Quarantine",
"quarantine_desc": "This instance will send only public posts to the following instances:", "quarantine_desc": "This instance will send only public posts to the following instances:",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline", "ftl_removal": "Removal from \"Known Network\" Timeline",
"ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:", "ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:",
"media_removal": "Media Removal", "media_removal": "Media Removal",
"media_removal_desc": "This instance removes media from posts on the following instances:", "media_removal_desc": "This instance removes media from posts on the following instances:",
"media_nsfw": "Media Force-set As Sensitive", "media_nsfw": "Media force-set as sensitive",
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:" "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
} }
}, },
@ -118,12 +118,13 @@
"about": "About", "about": "About",
"administration": "Administration", "administration": "Administration",
"back": "Back", "back": "Back",
"friend_requests": "Follow Requests", "friend_requests": "Follow requests",
"mentions": "Mentions", "mentions": "Mentions",
"interactions": "Interactions", "interactions": "Interactions",
"dms": "Direct Messages", "dms": "Direct messages",
"public_tl": "Public Timeline", "public_tl": "Public timeline",
"timeline": "Timeline", "timeline": "Timeline",
"home_timeline": "Home timeline",
"twkn": "Known Network", "twkn": "Known Network",
"bookmarks": "Bookmarks", "bookmarks": "Bookmarks",
"user_search": "User Search", "user_search": "User Search",
@ -148,8 +149,8 @@
"reacted_with": "reacted with {0}" "reacted_with": "reacted with {0}"
}, },
"polls": { "polls": {
"add_poll": "Add Poll", "add_poll": "Add poll",
"add_option": "Add Option", "add_option": "Add option",
"option": "Option", "option": "Option",
"votes": "votes", "votes": "votes",
"people_voted_count": "{count} person voted | {count} people voted", "people_voted_count": "{count} person voted | {count} people voted",
@ -178,7 +179,7 @@
"storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies." "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
}, },
"interactions": { "interactions": {
"favs_repeats": "Repeats and Favorites", "favs_repeats": "Repeats and favorites",
"follows": "New follows", "follows": "New follows",
"moves": "User migrates", "moves": "User migrates",
"load_older": "Load older interactions" "load_older": "Load older interactions"
@ -200,6 +201,7 @@
"direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting", "posting": "Posting",
"post": "Post",
"preview": "Preview", "preview": "Preview",
"preview_empty": "Empty", "preview_empty": "Empty",
"empty_status_error": "Can't post an empty status with no files", "empty_status_error": "Can't post an empty status with no files",
@ -210,10 +212,10 @@
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network" "unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
}, },
"scope": { "scope": {
"direct": "Direct - Post to mentioned users only", "direct": "Direct - post to mentioned users only",
"private": "Followers-only - Post to followers only", "private": "Followers-only - post to followers only",
"public": "Public - Post to public timelines", "public": "Public - post to public timelines",
"unlisted": "Unlisted - Do not post to public timelines" "unlisted": "Unlisted - do not post to public timelines"
} }
}, },
"registration": { "registration": {
@ -230,6 +232,7 @@
"bio_placeholder": "e.g.\nHi, I'm Lain.\nIm an anime girl living in suburban Japan. You may know me from the Wired.", "bio_placeholder": "e.g.\nHi, I'm Lain.\nIm an anime girl living in suburban Japan. You may know me from the Wired.",
"reason": "Reason to register", "reason": "Reason to register",
"reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.", "reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.",
"register": "Register",
"validations": { "validations": {
"username_required": "cannot be left blank", "username_required": "cannot be left blank",
"fullname_required": "cannot be left blank", "fullname_required": "cannot be left blank",
@ -249,6 +252,7 @@
}, },
"settings": { "settings": {
"app_name": "App name", "app_name": "App name",
"save": "Save changes",
"security": "Security", "security": "Security",
"setting_changed": "Setting is different from default", "setting_changed": "Setting is different from default",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity", "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
@ -277,7 +281,7 @@
"attachmentRadius": "Attachments", "attachmentRadius": "Attachments",
"attachments": "Attachments", "attachments": "Attachments",
"avatar": "Avatar", "avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)", "avatarAltRadius": "Avatars (notifications)",
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Background", "background": "Background",
"bio": "Bio", "bio": "Bio",
@ -299,10 +303,10 @@
"cGreen": "Green (Retweet)", "cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)", "cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)", "cRed": "Red (Cancel)",
"change_email": "Change Email", "change_email": "Change email",
"change_email_error": "There was an issue changing your email.", "change_email_error": "There was an issue changing your email.",
"changed_email": "Email changed successfully!", "changed_email": "Email changed successfully!",
"change_password": "Change Password", "change_password": "Change password",
"change_password_error": "There was an issue changing your password.", "change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!", "changed_password": "Password changed successfully!",
"chatMessageRadius": "Chat message", "chatMessageRadius": "Chat message",
@ -313,9 +317,9 @@
"current_mascot": "Your current mascot", "current_mascot": "Your current mascot",
"current_password": "Current password", "current_password": "Current password",
"mutes_and_blocks": "Mutes and Blocks", "mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data Import / Export", "data_import_export_tab": "Data import / export",
"default_vis": "Default visibility scope", "default_vis": "Default visibility scope",
"delete_account": "Delete Account", "delete_account": "Delete account",
"delete_account_description": "Permanently delete your data and deactivate your account.", "delete_account_description": "Permanently delete your data and deactivate your account.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.",
@ -368,18 +372,18 @@
"play_videos_in_modal": "Play videos in a popup frame", "play_videos_in_modal": "Play videos in a popup frame",
"profile_fields": { "profile_fields": {
"label": "Profile metadata", "label": "Profile metadata",
"add_field": "Add Field", "add_field": "Add field",
"name": "Label", "name": "Label",
"value": "Content" "value": "Content"
}, },
"use_contain_fit": "Don't crop the attachment in thumbnails", "use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name", "name": "Name",
"name_bio": "Name & Bio", "name_bio": "Name & bio",
"new_email": "New Email", "new_email": "New email",
"new_password": "New password", "new_password": "New password",
"notification_visibility": "Types of notifications to show", "notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows", "notification_visibility_follows": "Follows",
"notification_visibility_likes": "Likes", "notification_visibility_likes": "Favorites",
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates", "notification_visibility_moves": "User Migrates",
@ -391,19 +395,19 @@
"hide_followers_description": "Don't show who's following me", "hide_followers_description": "Don't show who's following me",
"hide_follows_count_description": "Don't show follow count", "hide_follows_count_description": "Don't show follow count",
"hide_followers_count_description": "Don't show follower count", "hide_followers_count_description": "Don't show follower count",
"show_admin_badge": "Show Admin badge in my profile", "show_admin_badge": "Show \"Admin\" badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile", "show_moderator_badge": "Show \"Moderator\" badge in my profile",
"nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses", "nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses",
"oauth_tokens": "OAuth tokens", "oauth_tokens": "OAuth tokens",
"token": "Token", "token": "Token",
"refresh_token": "Refresh Token", "refresh_token": "Refresh token",
"valid_until": "Valid Until", "valid_until": "Valid until",
"revoke_token": "Revoke", "revoke_token": "Revoke",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets", "presets": "Presets",
"profile_background": "Profile Background", "profile_background": "Profile background",
"profile_banner": "Profile Banner", "profile_banner": "Profile banner",
"profile_tab": "Profile", "profile_tab": "Profile",
"radii_help": "Set up interface edge rounding (in pixels)", "radii_help": "Set up interface edge rounding (in pixels)",
"replies_in_timeline": "Replies in timeline", "replies_in_timeline": "Replies in timeline",
@ -617,8 +621,8 @@
}, },
"version": { "version": {
"title": "Version", "title": "Version",
"backend_version": "Backend Version", "backend_version": "Backend version",
"frontend_version": "Frontend Version" "frontend_version": "Frontend version"
} }
}, },
"time": { "time": {
@ -666,7 +670,9 @@
"reload": "Reload", "reload": "Reload",
"up_to_date": "Up-to-date", "up_to_date": "Up-to-date",
"no_more_statuses": "No more statuses", "no_more_statuses": "No more statuses",
"no_statuses": "No statuses" "no_statuses": "No statuses",
"socket_reconnected": "Realtime connection established",
"socket_broke": "Realtime connection lost: CloseEvent code {0}"
}, },
"status": { "status": {
"favorites": "Favorites", "favorites": "Favorites",
@ -750,10 +756,16 @@
"quarantine": "Disallow user posts from federating", "quarantine": "Disallow user posts from federating",
"delete_user": "Delete user", "delete_user": "Delete user",
"delete_user_confirmation": "Are you absolutely sure? This action cannot be undone." "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
},
"highlight": {
"disabled": "No highlight",
"solid": "Solid bg",
"striped": "Striped bg",
"side": "Side stripe"
} }
}, },
"user_profile": { "user_profile": {
"timeline_title": "User Timeline", "timeline_title": "User timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.", "profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile." "profile_loading_error": "Sorry, there was an error loading this profile."
}, },
@ -771,7 +783,7 @@
"who_to_follow": "Who to follow" "who_to_follow": "Who to follow"
}, },
"tool_tip": { "tool_tip": {
"media_upload": "Upload Media", "media_upload": "Upload media",
"repeat": "Repeat", "repeat": "Repeat",
"reply": "Reply", "reply": "Reply",
"favorite": "Favorite", "favorite": "Favorite",

View file

@ -28,7 +28,6 @@ import pushNotifications from './lib/push_notifications_plugin.js'
import messages from './i18n/messages.js' import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside' import VueClickOutside from 'v-click-outside'
import PortalVue from 'portal-vue' import PortalVue from 'portal-vue'
import VBodyScrollLock from './directives/body_scroll_lock' import VBodyScrollLock from './directives/body_scroll_lock'
@ -42,7 +41,6 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0]
Vue.use(Vuex) Vue.use(Vuex)
Vue.use(VueRouter) Vue.use(VueRouter)
Vue.use(VueI18n) Vue.use(VueI18n)
Vue.use(VueChatScroll)
Vue.use(VueClickOutside) Vue.use(VueClickOutside)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VBodyScrollLock) Vue.use(VBodyScrollLock)

View file

@ -3,8 +3,11 @@ import { WSConnectionStatus } from '../services/api/api.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { Socket } from 'phoenix' import { Socket } from 'phoenix'
const retryTimeout = (multiplier) => 1000 * multiplier
const api = { const api = {
state: { state: {
retryMultiplier: 1,
backendInteractor: backendInteractorService(), backendInteractor: backendInteractorService(),
fetchers: {}, fetchers: {},
socket: null, socket: null,
@ -34,18 +37,43 @@ const api = {
}, },
setMastoUserSocketStatus (state, value) { setMastoUserSocketStatus (state, value) {
state.mastoUserSocketStatus = value state.mastoUserSocketStatus = value
},
incrementRetryMultiplier (state) {
state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
},
resetRetryMultiplier (state) {
state.retryMultiplier = 1
} }
}, },
actions: { actions: {
// Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets /**
enableMastoSockets (store) { * Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
const { state, dispatch } = store *
if (state.mastoUserSocket) return * @param {Boolean} [initial] - whether this enabling happened at boot time or not
*/
enableMastoSockets (store, initial) {
const { state, dispatch, commit } = store
// Do not initialize unless nonexistent or closed
if (
state.mastoUserSocket &&
![
WebSocket.CLOSED,
WebSocket.CLOSING
].includes(state.mastoUserSocket.getState())
) {
return
}
if (initial) {
commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL)
} else {
commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING)
}
return dispatch('startMastoUserSocket') return dispatch('startMastoUserSocket')
}, },
disableMastoSockets (store) { disableMastoSockets (store) {
const { state, dispatch } = store const { state, dispatch, commit } = store
if (!state.mastoUserSocket) return if (!state.mastoUserSocket) return
commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
return dispatch('stopMastoUserSocket') return dispatch('stopMastoUserSocket')
}, },
@ -91,11 +119,29 @@ const api = {
} }
) )
state.mastoUserSocket.addEventListener('open', () => { state.mastoUserSocket.addEventListener('open', () => {
// Do not show notification when we just opened up the page
if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) {
dispatch('pushGlobalNotice', {
level: 'success',
messageKey: 'timeline.socket_reconnected',
timeout: 5000
})
}
// Stop polling if we were errored or disabled
if (new Set([
WSConnectionStatus.ERROR,
WSConnectionStatus.DISABLED
]).has(state.mastoUserSocketStatus)) {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
dispatch('stopFetchingChats')
}
commit('resetRetryMultiplier')
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED) commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
}) })
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error) console.error('Error in MastoAPI websocket:', error)
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) // TODO is this needed?
dispatch('clearOpenedChats') dispatch('clearOpenedChats')
}) })
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
@ -106,14 +152,26 @@ const api = {
const { code } = closeEvent const { code } = closeEvent
if (ignoreCodes.has(code)) { if (ignoreCodes.has(code)) {
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
} else { } else {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`) console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' }) setTimeout(() => {
dispatch('startFetchingNotifications') dispatch('startMastoUserSocket')
dispatch('startFetchingChats') }, retryTimeout(state.retryMultiplier))
dispatch('restartMastoUserSocket') commit('incrementRetryMultiplier')
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingChats')
dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'timeline.socket_broke',
messageArgs: [code],
timeout: 5000
})
}
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
} }
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
dispatch('clearOpenedChats') dispatch('clearOpenedChats')
}) })
resolve() resolve()
@ -122,15 +180,6 @@ const api = {
} }
}) })
}, },
restartMastoUserSocket ({ dispatch }) {
// This basically starts MastoAPI user socket and stops conventional
// fetchers when connection reestablished
return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
dispatch('stopFetchingChats')
})
},
stopMastoUserSocket ({ state, dispatch }) { stopMastoUserSocket ({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications') dispatch('startFetchingNotifications')
@ -156,6 +205,13 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher }) store.commit('removeFetcher', { fetcherName: timeline, fetcher })
}, },
fetchTimeline (store, timeline, { ...rest }) {
store.state.backendInteractor.fetchTimeline({
store,
timeline,
...rest
})
},
// Notifications // Notifications
startFetchingNotifications (store) { startFetchingNotifications (store) {
@ -168,6 +224,12 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
}, },
fetchNotifications (store, { ...rest }) {
store.state.backendInteractor.fetchNotifications({
store,
...rest
})
},
// Follow requests // Follow requests
startFetchingFollowRequests (store) { startFetchingFollowRequests (store) {

View file

@ -18,6 +18,7 @@ const chat = {
actions: { actions: {
initializeChat (store, socket) { initializeChat (store, socket) {
const channel = socket.channel('chat:public') const channel = socket.channel('chat:public')
channel.on('new_msg', (msg) => { channel.on('new_msg', (msg) => {
store.commit('addMessage', msg) store.commit('addMessage', msg)
}) })

View file

@ -44,7 +44,7 @@ export const defaultState = {
likes: true, likes: true,
repeats: true, repeats: true,
moves: true, moves: true,
emojiReactions: false, emojiReactions: true,
followRequest: true, followRequest: true,
chatMention: true chatMention: true
}, },

View file

@ -557,9 +557,10 @@ const users = {
} }
if (store.getters.mergedConfig.useStreamingApi) { if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('enableMastoSockets').catch((error) => { store.dispatch('fetchTimeline', 'friends', { since: null })
store.dispatch('fetchNotifications', { since: null })
store.dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error) console.error('Failed initializing MastoAPI Streaming socket', error)
startPolling()
}).then(() => { }).then(() => {
store.dispatch('fetchChats', { latest: true }) store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)

View file

@ -1167,6 +1167,7 @@ export const ProcessedWS = ({
// 1000 = Normal Closure // 1000 = Normal Closure
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
eventTarget.getState = () => socket.readyState
return eventTarget return eventTarget
} }
@ -1198,7 +1199,10 @@ export const handleMastoWS = (wsEvent) => {
export const WSConnectionStatus = Object.freeze({ export const WSConnectionStatus = Object.freeze({
'JOINED': 1, 'JOINED': 1,
'CLOSED': 2, 'CLOSED': 2,
'ERROR': 3 'ERROR': 3,
'DISABLED': 4,
'STARTING': 5,
'STARTING_INITIAL': 6
}) })
const chats = ({ credentials }) => { const chats = ({ credentials }) => {

View file

@ -1,17 +1,25 @@
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js' import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const backendInteractorService = credentials => ({ const backendInteractorService = credentials => ({
startFetchingTimeline ({ timeline, store, userId = false, tag }) { startFetchingTimeline ({ timeline, store, userId = false, tag }) {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) return timelineFetcher.startFetching({ timeline, store, credentials, userId, tag })
},
fetchTimeline (args) {
return timelineFetcher.fetchAndUpdate({ ...args, credentials })
}, },
startFetchingNotifications ({ store }) { startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials }) return notificationsFetcher.startFetching({ store, credentials })
}, },
fetchNotifications (args) {
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingFollowRequests ({ store }) { startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials }) return followRequestFetcher.startFetching({ store, credentials })
}, },

View file

@ -5,7 +5,7 @@ const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older }) store.dispatch('addNewNotifications', { notifications, older })
} }
const fetchAndUpdate = ({ store, credentials, older = false }) => { const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials } const args = { credentials }
const { getters } = store const { getters } = store
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
@ -22,8 +22,10 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
return fetchNotifications({ store, args, older }) return fetchNotifications({ store, args, older })
} else { } else {
// fetch new notifications // fetch new notifications
if (timelineData.maxId !== Number.POSITIVE_INFINITY) { if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
args['since'] = timelineData.maxId args['since'] = timelineData.maxId
} else if (since !== null) {
args['since'] = since
} }
const result = fetchNotifications({ store, args, older }) const result = fetchNotifications({ store, args, older })

View file

@ -616,6 +616,23 @@ export const SLOT_INHERITANCE = {
textColor: true textColor: true
}, },
alertSuccess: {
depends: ['cGreen'],
opacity: 'alert'
},
alertSuccessText: {
depends: ['text'],
layer: 'alert',
variant: 'alertSuccess',
textColor: true
},
alertSuccessPanelText: {
depends: ['panelText'],
layer: 'alertPanel',
variant: 'alertSuccess',
textColor: true
},
alertNeutral: { alertNeutral: {
depends: ['text'], depends: ['text'],
opacity: 'alert' opacity: 'alert'
@ -656,6 +673,17 @@ export const SLOT_INHERITANCE = {
textColor: true textColor: true
}, },
alertPopupSuccess: {
depends: ['alertSuccess'],
opacity: 'alertPopup'
},
alertPopupSuccessText: {
depends: ['alertSuccessText'],
layer: 'popover',
variant: 'alertPopupSuccess',
textColor: true
},
alertPopupNeutral: { alertPopupNeutral: {
depends: ['alertNeutral'], depends: ['alertNeutral'],
opacity: 'alertPopup' opacity: 'alertPopup'

View file

@ -23,7 +23,8 @@ const fetchAndUpdate = ({
showImmediately = false, showImmediately = false,
userId = false, userId = false,
tag = false, tag = false,
until until,
since
}) => { }) => {
const args = { timeline, credentials } const args = { timeline, credentials }
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
@ -35,7 +36,11 @@ const fetchAndUpdate = ({
if (older) { if (older) {
args['until'] = until || timelineData.minId args['until'] = until || timelineData.minId
} else { } else {
args['since'] = timelineData.maxId if (since === undefined) {
args['since'] = timelineData.maxId
} else if (since !== null) {
args['since'] = since
}
} }
args['userId'] = userId args['userId'] = userId

View file

@ -8923,10 +8923,6 @@ void-elements@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
vue-chat-scroll@^1.2.1:
version "1.3.5"
resolved "https://registry.yarnpkg.com/vue-chat-scroll/-/vue-chat-scroll-1.3.5.tgz#a5ee5bae5058f614818a96eac5ee3be4394a2f68"
vue-eslint-parser@^5.0.0: vue-eslint-parser@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1"