From 835eaf33b194b25c41ed24815e6e5cd5eb4570cd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 Jan 2021 21:28:34 +0200 Subject: [PATCH 01/11] fix local dev websockets --- CHANGELOG.md | 1 + config/index.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7164eb26..0c8d4c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 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 ## [2.2.2] - 2020-12-22 ### Added diff --git a/config/index.js b/config/index.js index ccec4196..7cb87c3b 100644 --- a/config/index.js +++ b/config/index.js @@ -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) { From adc3b17fe0ba149386d83ee85f908578609bd676 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 Jan 2021 21:29:12 +0200 Subject: [PATCH 02/11] add success global notice style/level --- src/App.scss | 9 ++++++ .../global_notice_list/global_notice_list.vue | 8 ++++++ src/services/theme_data/pleromafe.js | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/App.scss b/src/App.scss index 2a1d7b1b..a3b1c87a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -698,6 +698,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 { diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 049e23db..a45f4586 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -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); diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index bec1eebd..14aac975 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -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' From 64fa662644dd385dd746f2b309eef6287f5eae04 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 Jan 2021 21:31:57 +0200 Subject: [PATCH 03/11] added notices for ws events --- CHANGELOG.md | 1 + src/i18n/en.json | 5 ++++- src/modules/api.js | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8d4c19..6160c5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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) ## [2.2.2] - 2020-12-22 ### Added diff --git a/src/i18n/en.json b/src/i18n/en.json index 26dd6144..5d613ea9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -646,7 +646,10 @@ "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}", + "socket_closed": "No realtime connection, updates can arrive with a delaye until connection is re-established" }, "status": { "favorites": "Favorites", diff --git a/src/modules/api.js b/src/modules/api.js index 08485a30..01c65e49 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -91,12 +91,29 @@ const api = { } ) state.mastoUserSocket.addEventListener('open', () => { + // Do not show notification when we just opened up the page + if (state.mastoUserSocketStatus !== null) { + dispatch('pushGlobalNotice', { + level: 'success', + messageKey: 'timeline.socket_reconnected', + timeout: 5000 + }) + } commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED) }) state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) dispatch('clearOpenedChats') + /* Since data in WS event for error is useless it's better to show + * generic warning instead of in "close" which actually has some + * useful data + */ + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.socket_closed', + timeout: 5000 + }) }) state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { const ignoreCodes = new Set([ @@ -112,6 +129,12 @@ const api = { dispatch('startFetchingNotifications') dispatch('startFetchingChats') dispatch('restartMastoUserSocket') + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.socket_broke', + messageArgs: [code], + timeout: 5000 + }) } commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) dispatch('clearOpenedChats') From 48bef143d86b02d7feeeac3e1faa0e5e00e09ea6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 Jan 2021 21:33:20 +0200 Subject: [PATCH 04/11] fix not being able to re-enable sockets until page refresh --- CHANGELOG.md | 1 + src/modules/api.js | 11 ++++++++++- src/services/api/api.service.js | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6160c5f4..0b9c7f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 ## [2.2.2] - 2020-12-22 ### Added diff --git a/src/modules/api.js b/src/modules/api.js index 01c65e49..2be0f236 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -40,7 +40,16 @@ const api = { // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets enableMastoSockets (store) { const { state, dispatch } = store - if (state.mastoUserSocket) return + // Do not initialize unless nonexistent or closed + if ( + state.mastoUserSocket && + ![ + WebSocket.CLOSED, + WebSocket.CLOSING + ].includes(state.mastoUserSocket.getState()) + ) { + return + } return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index f4483149..d3d5c68d 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1152,6 +1152,7 @@ export const ProcessedWS = ({ // 1000 = Normal Closure eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } + eventTarget.getState = () => socket.readyState return eventTarget } From 9a8bc245a6f76f1a41da9d05408dadc36625ffe9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 Jan 2021 22:17:10 +0200 Subject: [PATCH 05/11] fixed few-posts TLs when streaming is enabled --- CHANGELOG.md | 1 + src/components/notifications/notifications.js | 5 ----- src/modules/api.js | 13 +++++++++++++ src/modules/users.js | 2 ++ .../backend_interactor_service.js | 12 ++++++++++-- .../notifications_fetcher.service.js | 6 ++++-- .../timeline_fetcher/timeline_fetcher.service.js | 9 +++++++-- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9c7f68..4d0ca7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 ## [2.2.2] - 2020-12-22 ### Added diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 49258563..d6bd6375 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -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' diff --git a/src/modules/api.js b/src/modules/api.js index 2be0f236..b482637d 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -188,6 +188,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) { @@ -200,6 +207,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) { diff --git a/src/modules/users.js b/src/modules/users.js index 655db4c7..ac52be1f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -547,6 +547,8 @@ const users = { } if (store.getters.mergedConfig.useStreamingApi) { + store.dispatch('fetchTimeline', 'friends', { since: null }) + store.dispatch('fetchNotifications', { since: null }) store.dispatch('enableMastoSockets').catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) startPolling() diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 45e6bd0e..4a40f5b5 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -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 }) }, diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index beeb167c..f83f871e 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -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 }) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 921df3ed..46bba41a 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -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 From 98cb9abac773e8fbce94eafea1025919b6f26360 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson Date: Mon, 22 Feb 2021 16:24:04 +0200 Subject: [PATCH 06/11] Add timeline menu toggle to nav panel --- CHANGELOG.md | 2 + src/components/nav_panel/nav_panel.js | 36 ++++++---- src/components/nav_panel/nav_panel.vue | 70 +++++++++++++++---- src/components/timeline_menu/timeline_menu.js | 31 ++------ .../timeline_menu/timeline_menu.vue | 48 +------------ .../timeline_menu/timeline_menu_content.js | 29 ++++++++ .../timeline_menu/timeline_menu_content.vue | 66 +++++++++++++++++ src/i18n/en.json | 1 + 8 files changed, 183 insertions(+), 100 deletions(-) create mode 100644 src/components/timeline_menu/timeline_menu_content.js create mode 100644 src/components/timeline_menu/timeline_menu_content.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4be7da..15e63d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,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 ## [2.2.3] - 2021-01-18 ### Added diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 81d49cc2..37bcb409 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -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,16 +34,20 @@ 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] - }, - 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, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 0c83d0fe..7ae7b1d6 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -3,19 +3,33 @@
  • - {{ $t("nav.timelines") }} - + + +
    + +
  • - +
  • - +
  • - +
  • - + 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 - }) } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index 3c86842b..22dc3432 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -13,53 +13,7 @@ slot="content" class="timeline-menu-popover panel panel-default" > -
      -
    • - - {{ $t("nav.timeline") }} - -
    • -
    • - - {{ $t("nav.bookmarks") }} - -
    • -
    • - - {{ $t("nav.dms") }} - -
    • -
    • - - {{ $t("nav.public_tl") }} - -
    • -
    • - - {{ $t("nav.twkn") }} - -
    • -
    +
state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default TimelineMenuContent diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue new file mode 100644 index 00000000..bed1b679 --- /dev/null +++ b/src/components/timeline_menu/timeline_menu_content.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 4cc2af0e..45280eff 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -124,6 +124,7 @@ "dms": "Direct Messages", "public_tl": "Public Timeline", "timeline": "Timeline", + "home_timeline": "Home Timeline", "twkn": "Known Network", "bookmarks": "Bookmarks", "user_search": "User Search", From 92a9ce67c595bf54a95cca917ba07772bec0f041 Mon Sep 17 00:00:00 2001 From: Shpuld Shpludson Date: Sat, 27 Feb 2021 18:23:11 +0000 Subject: [PATCH 07/11] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a599ddb..a294c106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Added reason field for registration when approval is required ->>>>>>> develop ## [2.2.3] - 2021-01-18 ### Added From 90afcd3420a911856bde794f2b4dc1380a1a0751 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 8 Mar 2021 22:24:39 +0200 Subject: [PATCH 08/11] WIP some work on making errors less spammy --- src/i18n/en.json | 3 +- src/modules/api.js | 44 +++++++++++-------- .../notifications_fetcher.service.js | 14 +++--- .../timeline_fetcher.service.js | 14 +++--- 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index 217ac7c8..635bfd56 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -664,8 +664,7 @@ "no_more_statuses": "No more statuses", "no_statuses": "No statuses", "socket_reconnected": "Realtime connection established", - "socket_broke": "Realtime connection lost: CloseEvent code {0}", - "socket_closed": "No realtime connection, updates can arrive with a delaye until connection is re-established" + "socket_broke": "Realtime connection lost: CloseEvent code {0}" }, "status": { "favorites": "Favorites", diff --git a/src/modules/api.js b/src/modules/api.js index b482637d..e392d5b0 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -5,6 +5,8 @@ import { Socket } from 'phoenix' const api = { state: { + connectionBroken: false, + retryMultiplier: 1, backendInteractor: backendInteractorService(), fetchers: {}, socket: null, @@ -34,12 +36,18 @@ const api = { }, setMastoUserSocketStatus (state, value) { state.mastoUserSocketStatus = value + }, + recoverConnection (state) { + state.connectionBroken = false + }, + breakConnection (state) { + state.connectionBroken = true } }, actions: { // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets enableMastoSockets (store) { - const { state, dispatch } = store + const { state, dispatch, commit } = store // Do not initialize unless nonexistent or closed if ( state.mastoUserSocket && @@ -50,11 +58,13 @@ const api = { ) { return } + commit('recoverConnection') return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { - const { state, dispatch } = store + const { state, dispatch, commit } = store if (!state.mastoUserSocket) return + commit('recoverConnection') return dispatch('stopMastoUserSocket') }, @@ -102,6 +112,7 @@ const api = { state.mastoUserSocket.addEventListener('open', () => { // Do not show notification when we just opened up the page if (state.mastoUserSocketStatus !== null) { + commit('recoverConnection') dispatch('pushGlobalNotice', { level: 'success', messageKey: 'timeline.socket_reconnected', @@ -114,15 +125,6 @@ const api = { console.error('Error in MastoAPI websocket:', error) commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) dispatch('clearOpenedChats') - /* Since data in WS event for error is useless it's better to show - * generic warning instead of in "close" which actually has some - * useful data - */ - dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'timeline.socket_closed', - timeout: 5000 - }) }) state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { const ignoreCodes = new Set([ @@ -137,13 +139,19 @@ const api = { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') dispatch('startFetchingChats') - dispatch('restartMastoUserSocket') - dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'timeline.socket_broke', - messageArgs: [code], - timeout: 5000 - }) + setTimeout(() => { + console.log('TEST') + dispatch('restartMastoUserSocket') + }, 1000) + if (!state.connectionBroken) { + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.socket_broke', + messageArgs: [code], + timeout: 5000 + }) + } + commit('breakConnection') } commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) dispatch('clearOpenedChats') diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index f83f871e..fc5e76ba 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -57,12 +57,14 @@ const fetchNotifications = ({ store, args, older }) => { return notifications }) .catch((error) => { - store.dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'notifications.error', - messageArgs: [error.message], - timeout: 5000 - }) + if (!store.rootState.api.connectionBroken) { + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'notifications.error', + messageArgs: [error.message], + timeout: 5000 + }) + } }) } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 46bba41a..eb4a3e0d 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -66,12 +66,14 @@ const fetchAndUpdate = ({ return { statuses, pagination } }) .catch((error) => { - store.dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'timeline.error', - messageArgs: [error.message], - timeout: 5000 - }) + if (!store.rootState.api.connectionBroken) { + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.error', + messageArgs: [error.message], + timeout: 5000 + }) + } }) } From a8967d85bd7ae1541f1d4e41e95fc8fa111f2360 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 9 Mar 2021 02:38:10 +0200 Subject: [PATCH 09/11] streamlined WS flow, reduced spam amount related to WS reconnections --- src/modules/api.js | 69 +++++++++++-------- src/modules/users.js | 3 +- src/services/api/api.service.js | 5 +- .../notifications_fetcher.service.js | 14 ++-- .../timeline_fetcher.service.js | 14 ++-- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index e392d5b0..8654b90a 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -3,9 +3,10 @@ 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: { - connectionBroken: false, retryMultiplier: 1, backendInteractor: backendInteractorService(), fetchers: {}, @@ -37,16 +38,20 @@ const api = { setMastoUserSocketStatus (state, value) { state.mastoUserSocketStatus = value }, - recoverConnection (state) { - state.connectionBroken = false + incrementRetryMultiplier (state) { + state.retryMultiplier = Math.max(++state.retryMultiplier, 3) }, - breakConnection (state) { - state.connectionBroken = true + resetRetryMultiplier (state) { + state.retryMultiplier = 1 } }, 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 + * + * @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 ( @@ -58,13 +63,17 @@ const api = { ) { return } - commit('recoverConnection') + if (initial) { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL) + } else { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING) + } return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { const { state, dispatch, commit } = store if (!state.mastoUserSocket) return - commit('recoverConnection') + commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED) return dispatch('stopMastoUserSocket') }, @@ -111,19 +120,28 @@ const api = { ) state.mastoUserSocket.addEventListener('open', () => { // Do not show notification when we just opened up the page - if (state.mastoUserSocketStatus !== null) { - commit('recoverConnection') + 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 }) => { @@ -134,16 +152,17 @@ 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') setTimeout(() => { - console.log('TEST') - dispatch('restartMastoUserSocket') - }, 1000) - if (!state.connectionBroken) { + 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', @@ -151,9 +170,8 @@ const api = { timeout: 5000 }) } - commit('breakConnection') + commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) } - commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) dispatch('clearOpenedChats') }) resolve() @@ -162,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') diff --git a/src/modules/users.js b/src/modules/users.js index ac52be1f..8a764a16 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -549,9 +549,8 @@ const users = { if (store.getters.mergedConfig.useStreamingApi) { store.dispatch('fetchTimeline', 'friends', { since: null }) store.dispatch('fetchNotifications', { since: null }) - store.dispatch('enableMastoSockets').catch((error) => { + 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) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index d3d5c68d..436b8b0a 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1184,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 }) => { diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index fc5e76ba..f83f871e 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -57,14 +57,12 @@ const fetchNotifications = ({ store, args, older }) => { return notifications }) .catch((error) => { - if (!store.rootState.api.connectionBroken) { - store.dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'notifications.error', - messageArgs: [error.message], - timeout: 5000 - }) - } + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'notifications.error', + messageArgs: [error.message], + timeout: 5000 + }) }) } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index eb4a3e0d..46bba41a 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -66,14 +66,12 @@ const fetchAndUpdate = ({ return { statuses, pagination } }) .catch((error) => { - if (!store.rootState.api.connectionBroken) { - store.dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'timeline.error', - messageArgs: [error.message], - timeout: 5000 - }) - } + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.error', + messageArgs: [error.message], + timeout: 5000 + }) }) } From 47719571e454e3432e9d8fd34b5ce1ac513a187f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 15 Mar 2021 13:28:33 +0300 Subject: [PATCH 10/11] Use more specific button titles instead of general.submit - "Post" for post submission (we already had the button title be "Posting" when the post was being sent, so there was a weird inconsistency) - "Register" for registration submission - "Save changes" for usages in settings --- src/components/password_reset/password_reset.vue | 2 +- src/components/post_status_form/post_status_form.vue | 4 ++-- src/components/registration/registration.vue | 2 +- src/components/settings_modal/tabs/notifications_tab.vue | 2 +- src/components/settings_modal/tabs/profile_tab.vue | 6 +++--- .../settings_modal/tabs/security_tab/security_tab.vue | 6 +++--- src/i18n/en.json | 3 +++ 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index a931cb5a..3ffa5425 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -53,7 +53,7 @@ type="submit" class="btn button-default btn-block" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 73f6a4f1..6b490aee 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -272,7 +272,7 @@ disabled class="btn button-default" > - {{ $t('general.submit') }} + {{ $t('post_status.post') }}
- {{ $t('general.submit') }} + {{ $t('registration.register') }}
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index 8f8fe48e..7e0568ea 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -24,7 +24,7 @@ class="btn button-default" @click="updateNotificationSettings" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index 175a0219..bb3c301d 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -153,7 +153,7 @@ class="btn button-default" @click="updateProfile" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}
@@ -227,7 +227,7 @@ class="btn button-default" @click="submitBanner(banner)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}
@@ -266,7 +266,7 @@ class="btn button-default" @click="submitBackground(background)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index 56bea1f4..275d4616 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -22,7 +22,7 @@ class="btn button-default" @click="changeEmail" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}

{{ $t('settings.changed_email') }} @@ -60,7 +60,7 @@ class="btn button-default" @click="changePassword" > - {{ $t('general.submit') }} + {{ $t('settings.save') }}

{{ $t('settings.changed_password') }} @@ -133,7 +133,7 @@ class="btn button-default" @click="confirmDelete" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} diff --git a/src/i18n/en.json b/src/i18n/en.json index b3d8cd49..1ea27e26 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -201,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", @@ -231,6 +232,7 @@ "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m 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", @@ -250,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", From feb40ec5ffdbddd7becf7554df861bc2fc90ecf7 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 15 Mar 2021 13:49:03 +0300 Subject: [PATCH 11/11] Do not capitalize every word in random strings Capitalization like that is only ever used in article/book titles, I have not seen another website use it in interface elements. --- src/i18n/en.json | 70 ++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index b3d8cd49..517a0c61 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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,13 +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", + "home_timeline": "Home timeline", "twkn": "Known Network", "bookmarks": "Bookmarks", "user_search": "User Search", @@ -149,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", @@ -179,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" @@ -211,10 +211,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": { @@ -278,7 +278,7 @@ "attachmentRadius": "Attachments", "attachments": "Attachments", "avatar": "Avatar", - "avatarAltRadius": "Avatars (Notifications)", + "avatarAltRadius": "Avatars (notifications)", "avatarRadius": "Avatars", "background": "Background", "bio": "Bio", @@ -300,10 +300,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", @@ -312,9 +312,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.", @@ -367,14 +367,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", @@ -390,19 +390,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", @@ -614,8 +614,8 @@ }, "version": { "title": "Version", - "backend_version": "Backend Version", - "frontend_version": "Frontend Version" + "backend_version": "Backend version", + "frontend_version": "Frontend version" } }, "time": { @@ -758,7 +758,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 +776,7 @@ "who_to_follow": "Who to follow" }, "tool_tip": { - "media_upload": "Upload Media", + "media_upload": "Upload media", "repeat": "Repeat", "reply": "Reply", "favorite": "Favorite",