resolve merge conflicts and update fixes

This commit is contained in:
Absturztaube 2021-03-16 11:20:22 +01:00
commit 7f05730e9b
25 changed files with 411 additions and 182 deletions

View file

@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Display 'people voted' instead of 'votes' for multi-choice polls
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
- Renamed "Timeline" to "Home Timeline" to be more clear
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
- When opening emoji picker or react picker, it automatically focuses the search field
- Language picker now uses native language names
@ -35,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Follows/Followers tabs on user profiles now display the content properly.
- Handle punycode in screen names
- Fixed local dev mode having non-functional websockets in some cases
- Show notices for websocket events (errors, abnormal closures, reconnections)
- Fix not being able to re-enable websocket until page refresh
- Fix annoying issue where timeline might have few posts when streaming is enabled
### Changed
- Don't filter own posts when they hit your wordfilter

View file

@ -3,6 +3,11 @@ const path = require('path')
let settings = {}
try {
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(JSON.stringify(settings, null, 2))
} catch (e) {

View file

@ -706,6 +706,15 @@ nav {
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 {

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 {
background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text);

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

View file

@ -6,19 +6,33 @@
<div class="panel panel-default">
<ul>
<li v-if="currentUser || !privateMode">
<router-link
:to="{ name: timelinesRoute }"
:class="onTimelineRoute && 'router-link-active'"
<button
class="button-unstyled menu-item"
@click="toggleTimelines"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="home"
icon="stream"
/>{{ $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 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
fixed-width
class="fa-scale-110"
@ -27,7 +41,10 @@
</router-link>
</li>
<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
v-if="unreadChatCount"
class="badge badge-notification"
@ -42,7 +59,10 @@
</router-link>
</li>
<li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }">
<router-link
class="menu-item"
:to="{ name: 'friend-requests' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
@ -57,7 +77,10 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'about' }">
<router-link
class="menu-item"
:to="{ name: 'about' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
@ -94,14 +117,14 @@
border-color: var(--border, $fallback--border);
padding: 0;
&:first-child a {
&:first-child .menu-item {
border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
}
&:last-child a {
&:last-child .menu-item {
border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius;
@ -113,13 +136,15 @@
border: none;
}
a {
.menu-item {
display: block;
box-sizing: border-box;
align-items: stretch;
height: 3.5em;
line-height: 3.5em;
padding: 0 1em;
width: 100%;
color: $fallback--link;
color: var(--link, $fallback--link);
&:hover {
background-color: $fallback--lightBg;
@ -149,6 +174,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 {
margin-right: 0.8em;
}
@ -161,7 +205,7 @@
&.compact {
.panel {
height: 40px;
overflow: visible;
ul > li:hover > a:not(.router-link-active) > .button-icon {
color: var(--selectedMenuText,#b9b9ba);
@ -187,11 +231,21 @@
}
}
a {
.timelines-chevron {
display: none;
}
.timelines-background {
position: absolute;
}
a, button {
font-size: 0;
height: 100%;
display: flex;
position: relative;
padding-top: 7px;
padding-bottom: 7px;
}
.button-icon, svg.svg-inline--fa {

View file

@ -35,11 +35,6 @@ const Notifications = {
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
}
},
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.fetchAndUpdate({ store, credentials })
},
computed: {
mainClass () {
return this.minimalMode ? '' : 'panel panel-default'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +1,17 @@
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 {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown
)
library.add(faChevronDown)
// Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
return {
'friends': 'nav.timeline',
'friends': 'nav.home_timeline',
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
@ -33,7 +21,8 @@ export const timelineNames = () => {
const TimelineMenu = {
components: {
Popover
Popover,
TimelineMenuContent
},
data () {
return {
@ -41,9 +30,6 @@ const TimelineMenu = {
}
},
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
if (timelineNames()[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
@ -75,13 +61,6 @@ const TimelineMenu = {
const i18nkey = timelineNames()[this.$route.name]
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"
class="timeline-menu-popover panel panel-default"
>
<ul>
<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>
<TimelineMenuContent />
</div>
<div
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

@ -3,27 +3,27 @@
"mrf": {
"federation": "Federation",
"keyword": {
"keyword_policies": "Keyword Policies",
"keyword_policies": "Keyword policies",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"reject": "Reject",
"replace": "Replace",
"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:",
"simple": {
"simple_policies": "Instance-specific Policies",
"simple_policies": "Instance-specific policies",
"accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject",
"reject_desc": "This instance will not accept messages from the following instances:",
"quarantine": "Quarantine",
"quarantine_desc": "This instance will send only public posts to the following instances:",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
"ftl_removal": "Removal from \"Known Network\" Timeline",
"ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:",
"media_removal": "Media Removal",
"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:"
}
},
@ -118,12 +118,13 @@
"about": "About",
"administration": "Administration",
"back": "Back",
"friend_requests": "Follow Requests",
"friend_requests": "Follow requests",
"mentions": "Mentions",
"interactions": "Interactions",
"dms": "Direct Messages",
"public_tl": "Public Timeline",
"dms": "Direct messages",
"public_tl": "Public timeline",
"timeline": "Timeline",
"home_timeline": "Home timeline",
"twkn": "Known Network",
"bookmarks": "Bookmarks",
"user_search": "User Search",
@ -148,8 +149,8 @@
"reacted_with": "reacted with {0}"
},
"polls": {
"add_poll": "Add Poll",
"add_option": "Add Option",
"add_poll": "Add poll",
"add_option": "Add option",
"option": "Option",
"votes": "votes",
"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."
},
"interactions": {
"favs_repeats": "Repeats and Favorites",
"favs_repeats": "Repeats and favorites",
"follows": "New follows",
"moves": "User migrates",
"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_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting",
"post": "Post",
"preview": "Preview",
"preview_empty": "Empty",
"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"
},
"scope": {
"direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only",
"public": "Public - Post to public timelines",
"unlisted": "Unlisted - Do not post to public timelines"
"direct": "Direct - post to mentioned users only",
"private": "Followers-only - post to followers only",
"public": "Public - post to public timelines",
"unlisted": "Unlisted - do not post to public timelines"
}
},
"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.",
"reason": "Reason to register",
"reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.",
"register": "Register",
"validations": {
"username_required": "cannot be left blank",
"fullname_required": "cannot be left blank",
@ -249,6 +252,7 @@
},
"settings": {
"app_name": "App name",
"save": "Save changes",
"security": "Security",
"setting_changed": "Setting is different from default",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
@ -277,7 +281,7 @@
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarAltRadius": "Avatars (notifications)",
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
@ -299,10 +303,10 @@
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)",
"change_email": "Change Email",
"change_email": "Change email",
"change_email_error": "There was an issue changing your email.",
"changed_email": "Email changed successfully!",
"change_password": "Change Password",
"change_password": "Change password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
"chatMessageRadius": "Chat message",
@ -311,9 +315,9 @@
"confirm_new_password": "Confirm new password",
"current_password": "Current password",
"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",
"delete_account": "Delete Account",
"delete_account": "Delete 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_instructions": "Type your password in the input below to confirm account deletion.",
@ -369,14 +373,14 @@
"play_videos_in_modal": "Play videos in a popup frame",
"profile_fields": {
"label": "Profile metadata",
"add_field": "Add Field",
"add_field": "Add field",
"name": "Label",
"value": "Content"
},
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
"name_bio": "Name & Bio",
"new_email": "New Email",
"name_bio": "Name & bio",
"new_email": "New email",
"new_password": "New password",
"notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows",
@ -392,19 +396,19 @@
"hide_followers_description": "Don't show who's following me",
"hide_follows_count_description": "Don't show follow count",
"hide_followers_count_description": "Don't show follower count",
"show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile",
"show_admin_badge": "Show \"Admin\" 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",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"refresh_token": "Refresh token",
"valid_until": "Valid until",
"revoke_token": "Revoke",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
"profile_background": "Profile Background",
"profile_banner": "Profile Banner",
"profile_background": "Profile background",
"profile_banner": "Profile banner",
"profile_tab": "Profile",
"radii_help": "Set up interface edge rounding (in pixels)",
"replies_in_timeline": "Replies in timeline",
@ -616,8 +620,8 @@
},
"version": {
"title": "Version",
"backend_version": "Backend Version",
"frontend_version": "Frontend Version"
"backend_version": "Backend version",
"frontend_version": "Frontend version"
}
},
"time": {
@ -665,7 +669,9 @@
"reload": "Reload",
"up_to_date": "Up-to-date",
"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": {
"favorites": "Favorites",
@ -758,7 +764,7 @@
}
},
"user_profile": {
"timeline_title": "User Timeline",
"timeline_title": "User timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile."
},
@ -776,7 +782,7 @@
"who_to_follow": "Who to follow"
},
"tool_tip": {
"media_upload": "Upload Media",
"media_upload": "Upload media",
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",

View file

@ -3,8 +3,11 @@ import { WSConnectionStatus } from '../services/api/api.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { Socket } from 'phoenix'
const retryTimeout = (multiplier) => 1000 * multiplier
const api = {
state: {
retryMultiplier: 1,
backendInteractor: backendInteractorService(),
fetchers: {},
socket: null,
@ -34,18 +37,43 @@ const api = {
},
setMastoUserSocketStatus (state, value) {
state.mastoUserSocketStatus = value
},
incrementRetryMultiplier (state) {
state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
},
resetRetryMultiplier (state) {
state.retryMultiplier = 1
}
},
actions: {
// Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
enableMastoSockets (store) {
const { state, dispatch } = store
if (state.mastoUserSocket) return
/**
* Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
*
* @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')
},
disableMastoSockets (store) {
const { state, dispatch } = store
const { state, dispatch, commit } = store
if (!state.mastoUserSocket) return
commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
return dispatch('stopMastoUserSocket')
},
@ -91,11 +119,29 @@ const api = {
}
)
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)
})
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
// TODO is this needed?
dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
@ -106,14 +152,26 @@ const api = {
const { code } = closeEvent
if (ignoreCodes.has(code)) {
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
} else {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingChats')
dispatch('restartMastoUserSocket')
setTimeout(() => {
dispatch('startMastoUserSocket')
}, retryTimeout(state.retryMultiplier))
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')
})
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 }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
@ -156,6 +205,13 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
},
fetchTimeline (store, timeline, { ...rest }) {
store.state.backendInteractor.fetchTimeline({
store,
timeline,
...rest
})
},
// Notifications
startFetchingNotifications (store) {
@ -168,6 +224,12 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
fetchNotifications (store, { ...rest }) {
store.state.backendInteractor.fetchNotifications({
store,
...rest
})
},
// Follow requests
startFetchingFollowRequests (store) {

View file

@ -553,9 +553,10 @@ const users = {
}
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)
startPolling()
}).then(() => {
store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)

View file

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

View file

@ -1,17 +1,25 @@
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 followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const backendInteractorService = credentials => ({
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 }) {
return notificationsFetcher.startFetching({ store, credentials })
},
fetchNotifications (args) {
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},

View file

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

View file

@ -616,6 +616,23 @@ export const SLOT_INHERITANCE = {
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: {
depends: ['text'],
opacity: 'alert'
@ -656,6 +673,17 @@ export const SLOT_INHERITANCE = {
textColor: true
},
alertPopupSuccess: {
depends: ['alertSuccess'],
opacity: 'alertPopup'
},
alertPopupSuccessText: {
depends: ['alertSuccessText'],
layer: 'popover',
variant: 'alertPopupSuccess',
textColor: true
},
alertPopupNeutral: {
depends: ['alertNeutral'],
opacity: 'alertPopup'

View file

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